Skip to content

Commit

Permalink
Fix bug in winter_openapi: required dataclass fields weren't marked a…
Browse files Browse the repository at this point in the history
…s required. (#215)

* Move tests from tests/schema to tests/winter_openapi

* Correctly set required fields in openapi schema for dataclasses

* Update changelog and bump version to 9.5.3
  • Loading branch information
mofr authored Jun 8, 2022
1 parent 7a1c6fc commit 1f6b667
Show file tree
Hide file tree
Showing 15 changed files with 74 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [9.5.3] - 2022-06-08

Fix bug in winter_openapi: required dataclass fields weren't marked as required.

## [9.4.1] - 2021-09-16

Change build system to poetry
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "winter"
version = "9.5.2"
version = "9.5.3"
homepage = "https://github.com/WinterFramework/winter"
description = "Web Framework inspired by Spring Framework"
authors = ["Alexander Egorov <mofr@zond.org>"]
Expand Down
Empty file removed tests/schema/__init__.py
Empty file.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ def test_with_invalid_return_type():
build_response_schema(Controller.with_invalid_return_type)

assert repr(e.value) == (
'CanNotInspectReturnType(tests.schema.test_build_response_schema.Controller.with_invalid_return_type: '
'CanNotInspectReturnType(test_build_response_schema.Controller.with_invalid_return_type: '
"-> <class 'object'>: Unknown type: <class 'object'>)"
)
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from winter.web.routing import get_route
from winter_openapi.generation import build_response_schema
from winter_openapi.generation import build_responses_schemas
from ..controllers import ControllerWithExceptions
from ..controllers import ControllerWithProblemExceptions
from tests.controllers import ControllerWithExceptions
from tests.controllers import ControllerWithProblemExceptions


@pytest.mark.parametrize(
Expand Down Expand Up @@ -39,9 +39,11 @@ def simple_method(self) -> return_type: # pragma: no cover
ControllerWithExceptions, 'declared_and_thrown', {
'200': openapi.Schema(type=openapi.TYPE_STRING),
'400': openapi.Schema(
type=openapi.TYPE_OBJECT, properties={
type=openapi.TYPE_OBJECT,
properties={
'message': openapi.Schema(type=openapi.TYPE_STRING),
},
required=['message'],
),
},
),
Expand All @@ -67,12 +69,19 @@ def simple_method(self) -> return_type: # pragma: no cover
'403': openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'custom_field': openapi.Schema(type=openapi.TYPE_STRING),
'detail': openapi.Schema(type=openapi.TYPE_STRING),
'status': openapi.Schema(type=openapi.TYPE_INTEGER),
'title': openapi.Schema(type=openapi.TYPE_STRING),
'detail': openapi.Schema(type=openapi.TYPE_STRING),
'type': openapi.Schema(type=openapi.TYPE_STRING),
'status': openapi.Schema(type=openapi.TYPE_INTEGER),
'custom_field': openapi.Schema(type=openapi.TYPE_STRING),
},
required=[
'status',
'title',
'detail',
'type',
'custom_field',
],
),
},
),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dataclasses
from typing import Optional

from drf_yasg import openapi
from rest_framework import serializers
Expand All @@ -14,6 +15,8 @@
@dataclasses.dataclass
class UserDTO:
name: str
surname: str = ''
age: Optional[int] = None


class UserSerializer(serializers.Serializer):
Expand Down Expand Up @@ -79,7 +82,15 @@ def test_get_operation():
'name': {
'type': openapi.TYPE_STRING,
},
'surname': {
'type': openapi.TYPE_STRING,
},
'age': {
'type': openapi.TYPE_INTEGER,
'x-nullable': True,
},
},
required=['name'],
),
),
openapi.Parameter(
Expand All @@ -104,9 +115,17 @@ def test_get_operation():
type=openapi.TYPE_OBJECT,
properties={
'name': {
'type': 'string',
'type': openapi.TYPE_STRING,
},
'surname': {
'type': openapi.TYPE_STRING,
},
'age': {
'type': openapi.TYPE_INTEGER,
'x-nullable': True,
},
},
required=['name', 'surname', 'age'],
),
),
})
Expand All @@ -128,7 +147,11 @@ def test_get_operation_with_serializer():
View.post.route = route
reference_resolver = openapi.ReferenceResolver('definitions', 'parameters', force_init=True)
auto_schema = SwaggerAutoSchema(view, 'path', route.http_method, reference_resolver, 'request', {})

# Act
operation = auto_schema.get_operation(['test_app', 'post'])

# Assert
schema_ref = openapi.SchemaRef(
resolver=reference_resolver,
schema_name='User',
Expand All @@ -148,9 +171,17 @@ def test_get_operation_with_serializer():
type=openapi.TYPE_OBJECT,
properties={
'name': {
'type': 'string',
'type': openapi.TYPE_STRING,
},
'surname': {
'type': openapi.TYPE_STRING,
},
'age': {
'type': openapi.TYPE_INTEGER,
'x-nullable': True,
},
},
required=['name', 'surname', 'age'],
),
),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,6 @@ def test_get_argument_type_info_with_non_registered_type():
assert str(exception_info.value) == f'Unknown type: {hint_class}'


def test_get_openapi_schema():
type_info = TypeInfo(openapi.TYPE_BOOLEAN)
schema = openapi.Schema(type=openapi.TYPE_BOOLEAN)
assert type_info.get_openapi_schema() == schema


@pytest.mark.parametrize(('first', 'second', 'is_same'), (
(TypeInfo(openapi.TYPE_INTEGER), TypeInfo(openapi.TYPE_INTEGER), True),
(TypeInfo(openapi.TYPE_INTEGER), TypeInfo(openapi.TYPE_NUMBER), False),
Expand Down
2 changes: 1 addition & 1 deletion winter_openapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .annotations import global_exception
from .annotations import register_global_exception
from .enum_inspector import inspect_enum_class
from .inspectors import SwaggerAutoSchema
from .swagger_auto_schema import SwaggerAutoSchema
from .method_arguments_inspector import MethodArgumentsInspector
from .method_arguments_inspector import get_method_arguments_inspectors
from .method_arguments_inspector import register_controller_method_inspector
Expand Down
2 changes: 1 addition & 1 deletion winter_openapi/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def build_response_schema(method: ComponentMethod):
type_info = inspect_type(return_value_type)
except InspectorNotFound as e:
raise CanNotInspectReturnType(method, return_value_type, str(e))
return type_info.get_openapi_schema()
return type_info.get_openapi_schema(output=True)


def build_method_parameters(route: Route) -> List['openapi.Parameter']:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def _get_request_body_parameters(self, route: Route) -> List[openapi.Parameter]:
argument = method.get_argument(request_body_annotation.argument_name)
type_info = inspect_type(argument.type_)
title = get_schema_title(argument)
schema = openapi.Schema(title=title, **type_info.as_dict())
schema = type_info.get_openapi_schema(output=False, title=title)
return [openapi.Parameter(name='data', in_=openapi.IN_BODY, required=True, schema=schema)]
return []

Expand Down
20 changes: 17 additions & 3 deletions winter_openapi/type_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class TypeInfo:
child: Optional['TypeInfo'] = None
nullable: bool = False
properties: Dict[str, 'TypeInfo'] = dataclasses.field(default_factory=OrderedDict)
properties_defaults: Dict[str, object] = dataclasses.field(default_factory=dict)
enum: Optional[list] = None

def __eq__(self, other):
Expand Down Expand Up @@ -87,8 +88,16 @@ def as_dict(self):

return data

def get_openapi_schema(self):
return openapi.Schema(**self.as_dict())
def get_openapi_schema(self, output: bool, title: str = None) -> openapi.Schema:
if output:
required_properties = list(self.properties)
else:
required_properties = [
property_name
for property_name in self.properties
if property_name not in self.properties_defaults
]
return openapi.Schema(title=title, required=required_properties or None, **self.as_dict())


# noinspection PyUnusedLocal
Expand Down Expand Up @@ -206,7 +215,12 @@ def inspect_dataclass(hint_class) -> TypeInfo:
field.name: inspect_type(field.type)
for field in fields
}
return TypeInfo(type_=openapi.TYPE_OBJECT, properties=properties)
defaults = {
field.name: field.default
for field in fields
if field.default != dataclasses.MISSING
}
return TypeInfo(type_=openapi.TYPE_OBJECT, properties=properties, properties_defaults=defaults)


@register_type_inspector(object, checker=has_nested_type)
Expand Down

0 comments on commit 1f6b667

Please sign in to comment.