Skip to content

Commit

Permalink
Pick a crazy madjong stone
Browse files Browse the repository at this point in the history
  • Loading branch information
pristupa committed Jul 26, 2024
1 parent 84994c9 commit 5410629
Show file tree
Hide file tree
Showing 12 changed files with 416 additions and 153 deletions.
318 changes: 276 additions & 42 deletions tests/winter_openapi/test_api_request_and_response_spec.py

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions tests/winter_openapi/test_query_and_path_parameter_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import winter
from winter.web.routing import get_route
from winter_openapi import generate_openapi
from winter_openapi.generator import CanNotInspectType


class IntegerValueEnum(Enum):
Expand All @@ -31,7 +32,6 @@ class IntegerEnum(IntEnum):


param_with_diff_types = [
(object, {'schema': {'type': 'string'}, 'description': 'winter_openapi has failed to inspect the parameter'}),
(int, {'schema': {'format': 'int32', 'type': 'integer'}}),
(str, {'schema': {'type': 'string'}}),
(IntegerEnum, {'schema': {'enum': [1, 2], 'format': 'int32', 'type': 'integer'}}),
Expand Down Expand Up @@ -255,8 +255,13 @@ def simple_method(
}
route = get_route(_TestAPI.simple_method)
# Act
result = generate_openapi(title='title', version='1.0.0', routes=[route])
with pytest.raises(CanNotInspectType) as e:
generate_openapi(title='title', version='1.0.0', routes=[route])

# Assert
parameters = result["paths"]["/resource/"]["get"]["parameters"]
assert parameters == [expected_parameter]
assert str(e.value) == (
"test_query_and_path_parameter_spec._TestAPI.simple_method: Unknown type: "
"<class 'test_query_and_path_parameter_spec.test_custom_query_parameters_with_wrong_field_type."
"<locals>.UnknownType'>"
)

Empty file.
25 changes: 15 additions & 10 deletions winter_openapi/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ def __init__(self):
self._types_by_titles: Dict[str, Type] = {}

def get_schema_or_reference(self, type_: Type, output: bool) -> Union[Schema, Reference]:
type_info = inspect_type(type_)
schema = self._schemas.get((type_, output))
if not schema:
type_info = inspect_type(type_)
schema = convert_type_info_to_openapi_schema(type_info, output=output)
schema = convert_type_info_to_openapi_schema(type_info, output=output, schema_registry=self)
if not schema.title:
return schema
self._schemas[type_, output] = schema
Expand All @@ -62,7 +62,14 @@ def get_schema_or_reference(self, type_: Type, output: bool) -> Union[Schema, Re
elif self._types_by_titles[schema.title] != type_:
raise ValueError(f'Title {schema.title} for type {type_} is already used for another type: {self._types_by_titles[schema.title]}')

return Reference(ref='#/components/schemas/' + schema.title)
reference = Reference(ref='#/components/schemas/' + schema.title)

if type_info.nullable and schema.type == 'object':
# https://stackoverflow.com/questions/40920441/how-to-specify-a-property-can-be-null-or-a-reference-with-swagger
# Better solution, but not implemented yet https://github.com/OpenAPITools/openapi-generator/issues/9083
return Schema(nullable=True, allOf=[reference])

return reference

def get_schemas(self) -> Dict[str, Schema]:
return {
Expand Down Expand Up @@ -144,7 +151,7 @@ def _get_openapi_path(
schema_registry=schema_registry,
)
except InspectorNotFound as e:
raise CanNotInspectType(route.method, e.hint_cls, str(e))
raise CanNotInspectType(route.method, str(e))
path[route.http_method.lower()] = operation

return PathItem.parse_obj(path)
Expand All @@ -159,7 +166,7 @@ def _get_openapi_operation(
) -> Operation:
summary = route.method.docstring.short_description
description = route.method.docstring.long_description
operation_parameters = get_route_parameters(route)
operation_parameters = get_route_parameters(route, schema_registry)
operation_request_body = get_request_body_parameters(route, schema_registry)
operation_responses = get_responses_schemas(route, schema_registry)
return Operation(
Expand All @@ -178,10 +185,8 @@ class CanNotInspectType(Exception):
def __init__(
self,
method: ComponentMethod,
type_: Any,
message: str,
):
self._type = type_
self._message = message
self._method = method

Expand All @@ -191,7 +196,7 @@ def __repr__(self):
def __str__(self):
component_cls = self._method.component.component_cls
method_path = f'{component_cls.__module__}.{self._method.full_name}'
return f'{method_path}: -> {self._type}: {self._message}'
return f'{method_path}: {self._message}'


def get_url_path_without_prefix(url_path: str, path_prefix: str) -> str:
Expand Down Expand Up @@ -225,10 +230,10 @@ def get_url_path_tag(url_path: str, path_prefix: str) -> Optional[str]:
return url_path_tag


def get_route_parameters(route: Route) -> List[Parameter]:
def get_route_parameters(route: Route, schema_registry: SchemaRegistry) -> List[Parameter]:
parameters = []
for inspector in get_route_parameters_inspectors():
parameters += inspector.inspect_parameters(route)
parameters += inspector.inspect_parameters(route, schema_registry)
return parameters


Expand Down
2 changes: 2 additions & 0 deletions winter_openapi/inspection/type_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataclasses import field
from typing import Dict
from typing import Optional
from typing import Type

from .data_formats import DataFormat
from .data_types import DataTypes
Expand All @@ -11,6 +12,7 @@
@dataclass
class TypeInfo:
type_: DataTypes
hint_class: Type
format_: Optional[DataFormat] = None
child: Optional['TypeInfo'] = None
nullable: bool = False
Expand Down
48 changes: 29 additions & 19 deletions winter_openapi/inspectors/page_inspector.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import dataclasses
from typing import List
from typing import Optional

from winter.data.pagination import Page
from winter_openapi.inspection.data_formats import DataFormat
from winter_openapi.inspection.data_types import DataTypes
from winter_openapi.inspection.type_info import TypeInfo
from winter_openapi.inspectors.standard_types_inspectors import inspect_type
from winter_openapi.inspectors.standard_types_inspectors import register_type_inspector
Expand All @@ -18,22 +17,33 @@ def inspect_page(hint_class) -> TypeInfo:
child_type_info = inspect_type(child_class)
title = child_type_info.title or child_type_info.type_.capitalize()

return TypeInfo(
type_=DataTypes.OBJECT,
title=f'PageOf{title}',
properties={
'meta': TypeInfo(
type_=DataTypes.OBJECT,
title=f'PageMetaOf{title}',
properties={
'total_count': TypeInfo(type_=DataTypes.INTEGER),
'limit': TypeInfo(type_=DataTypes.INTEGER, nullable=True),
'offset': TypeInfo(type_=DataTypes.INTEGER, nullable=True),
'previous': TypeInfo(type_=DataTypes.STRING, format_=DataFormat.URI, nullable=True),
'next': TypeInfo(type_=DataTypes.STRING, format_=DataFormat.URI, nullable=True),
**{extra_field.name: inspect_type(extra_field.type) for extra_field in extra_fields},
PageMetaDataclass = dataclasses.dataclass(
type(
f'PageMetaOf{title}',
(),
{
'__annotations__': {
'total_count': int,
'limit': Optional[int],
'offset': Optional[int],
'previous': Optional[str],
'next': Optional[str],
**{extra_field.name: extra_field.type for extra_field in extra_fields},
},
),
'objects': inspect_type(List[child_class]),
},
},
),
)
PageDataclass = dataclasses.dataclass(
type(
f'PageOf{title}',
(),
{
'__annotations__': {
'meta': PageMetaDataclass,
'objects': List[child_class],
},
},
),
)

return inspect_type(PageDataclass)
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List
from typing import TYPE_CHECKING

from openapi_pydantic.v3.v3_0_3 import Parameter
from openapi_pydantic.v3.v3_0_3 import Schema
Expand All @@ -10,6 +11,9 @@
from winter_openapi.inspection.data_types import DataTypes
from .route_parameters_inspector import RouteParametersInspector

if TYPE_CHECKING:
from winter_openapi.generator import SchemaRegistry


class PagePositionArgumentsInspector(RouteParametersInspector):
def __init__(self, page_position_argument_resolver: PagePositionArgumentResolver):
Expand All @@ -31,7 +35,7 @@ def __init__(self, page_position_argument_resolver: PagePositionArgumentResolver
param_schema=Schema(type=DataTypes.INTEGER)
)

def inspect_parameters(self, route: 'Route') -> List[Parameter]:
def inspect_parameters(self, route: 'Route', schema_registry: 'SchemaRegistry') -> List[Parameter]:
parameters = []
has_page_position_argument = any(argument.type_ == PagePosition for argument in route.method.arguments)
if not has_page_position_argument:
Expand Down
30 changes: 13 additions & 17 deletions winter_openapi/inspectors/path_parameters_inspector.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
from typing import List
from typing import TYPE_CHECKING

from openapi_pydantic.v3.v3_0_3 import Parameter

from winter.core import ComponentMethodArgument
from winter.web.routing import Route
from winter_openapi.inspection.inspection import InspectorNotFound
from winter_openapi.inspection.inspection import inspect_type
from winter_openapi.type_info_converter import convert_type_info_to_openapi_schema
from .route_parameters_inspector import RouteParametersInspector
from ..inspection import DataTypes
from ..inspection import TypeInfo

if TYPE_CHECKING:
from winter_openapi.generator import SchemaRegistry


class PathParametersInspector(RouteParametersInspector):

def inspect_parameters(self, route: 'Route') -> List[Parameter]:
def inspect_parameters(self, route: 'Route', schema_registry: 'SchemaRegistry') -> List[Parameter]:
parameters = []

for argument in self._path_arguments(route):
openapi_parameter = self._convert_argument_to_openapi_parameter(argument)
openapi_parameter = self._convert_argument_to_openapi_parameter(argument, schema_registry)
parameters.append(openapi_parameter)

return parameters

def _convert_argument_to_openapi_parameter(self, argument: ComponentMethodArgument) -> Parameter:
try:
type_info = inspect_type(argument.type_)
description = argument.description
except InspectorNotFound:
type_info = TypeInfo(DataTypes.STRING)
description = 'winter_openapi has failed to inspect the parameter'

schema = convert_type_info_to_openapi_schema(type_info, output=False)
def _convert_argument_to_openapi_parameter(
self,
argument: ComponentMethodArgument,
schema_registry: 'SchemaRegistry',
) -> Parameter:
schema = schema_registry.get_schema_or_reference(argument.type_, output=False)
return Parameter(
name=argument.name,
description=description,
description=argument.description,
required=argument.required,
param_in='path',
default=argument.get_default(None),
Expand Down
49 changes: 23 additions & 26 deletions winter_openapi/inspectors/query_parameters_inspector.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import dataclasses
from typing import List
from typing import TYPE_CHECKING
from typing import Tuple

import dataclasses
from openapi_pydantic import Parameter

from winter.core import ComponentMethodArgument
from winter.web.query_parameters import QueryParameter
from winter.web.query_parameters.query_parameters_annotation import QueryParametersAnnotation
from winter.web.routing import Route
from winter_openapi.inspection import DataTypes
from winter_openapi.inspection import TypeInfo
from winter_openapi.inspection.inspection import InspectorNotFound
from winter_openapi.inspection.inspection import inspect_type
from winter_openapi.inspectors.route_parameters_inspector import RouteParametersInspector
from winter_openapi.type_info_converter import convert_type_info_to_openapi_schema
from .route_parameters_inspector import RouteParametersInspector

if TYPE_CHECKING:
from winter_openapi.generator import SchemaRegistry


class QueryParametersInspector(RouteParametersInspector):

def inspect_parameters(self, route: 'Route') -> List[Parameter]:
def inspect_parameters(self, route: 'Route', schema_registry: 'SchemaRegistry') -> List[Parameter]:
parameters = []

annotation = route.method.annotations.get_one_or_none(QueryParametersAnnotation)
Expand All @@ -27,11 +26,19 @@ def inspect_parameters(self, route: 'Route') -> List[Parameter]:
query_parameters_map = {query_parameter.name: query_parameter for query_parameter in query_parameters}
for field in dataclasses.fields(annotation.argument.type_):
query_parameter = query_parameters_map[field.name]
openapi_parameter = self._convert_dataclass_field_to_openapi_parameter(field, query_parameter)
openapi_parameter = self._convert_dataclass_field_to_openapi_parameter(
field,
query_parameter,
schema_registry,
)
parameters.append(openapi_parameter)
else:
for argument, query_parameter in self._query_arguments(route):
openapi_parameter = self._convert_argument_to_openapi_parameter(argument, query_parameter)
openapi_parameter = self._convert_argument_to_openapi_parameter(
argument,
query_parameter,
schema_registry,
)
parameters.append(openapi_parameter)

return parameters
Expand All @@ -40,17 +47,12 @@ def _convert_argument_to_openapi_parameter(
self,
argument: ComponentMethodArgument,
query_parameter: QueryParameter,
schema_registry: 'SchemaRegistry',
) -> Parameter:
try:
type_info = inspect_type(argument.type_)
description = argument.description
except InspectorNotFound:
type_info = TypeInfo(DataTypes.STRING)
description = 'winter_openapi has failed to inspect the parameter'
schema = convert_type_info_to_openapi_schema(type_info, output=False)
schema = schema_registry.get_schema_or_reference(argument.type_, output=False)
return Parameter(
name=query_parameter.name,
description=description,
description=argument.description,
required=argument.required,
param_in='query',
default=argument.get_default(None),
Expand All @@ -62,17 +64,12 @@ def _convert_dataclass_field_to_openapi_parameter(
self,
field: dataclasses.Field,
query_parameter: QueryParameter,
schema_registry: 'SchemaRegistry',
) -> Parameter:
try:
type_info = inspect_type(field.type)
description = ''
except InspectorNotFound:
type_info = TypeInfo(DataTypes.STRING)
description = 'winter_openapi has failed to inspect the parameter'
schema = convert_type_info_to_openapi_schema(type_info, output=False)
schema = schema_registry.get_schema_or_reference(field.type, output=False)
return Parameter(
name=query_parameter.name,
description=description,
description='',
required=field.default is dataclasses.MISSING,
param_in='query',
default=field.default,
Expand Down
11 changes: 8 additions & 3 deletions winter_openapi/inspectors/route_parameters_inspector.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import abc
import logging
from typing import List
from typing import TYPE_CHECKING

from openapi_pydantic import Parameter

from winter.web.routing import Route
import logging

class RouteParametersInspector(abc.ABC):
if TYPE_CHECKING:
from winter_openapi.generator import SchemaRegistry


class RouteParametersInspector(abc.ABC): # pragma: no cover

@abc.abstractmethod
def inspect_parameters(self, route: 'Route') -> List[Parameter]: # pragma: no cover
def inspect_parameters(self, route: 'Route', schema_registry: 'SchemaRegistry') -> List[Parameter]:
return []


Expand Down
Loading

0 comments on commit 5410629

Please sign in to comment.