Skip to content

Commit ecac517

Browse files
committed
Fix openapi schema sharing for Page types
1 parent f0232e8 commit ecac517

File tree

4 files changed

+115
-8
lines changed

4 files changed

+115
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [25.0.2] - 2024-08-01
8+
- Fix openapi schema sharing for Page types
9+
710
## [25.0.1] - 2024-07-28
811
- Fix openapi schema sharing between optional and non-optional types
912

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "winter"
3-
version = "25.0.1"
3+
version = "25.0.2"
44
homepage = "https://github.com/WinterFramework/winter"
55
description = "Web Framework with focus on python typing, dataclasses and modular design"
66
authors = ["Alexander Egorov <[email protected]>"]

tests/winter_openapi/test_api_request_and_response_spec.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ class DataclassWithUndefined:
581581
nested_2: Union[Dataclass, Undefined, None]
582582

583583

584-
def test_reuse_schema():
584+
def test_reuse_dataclass_schema():
585585
class _TestAPI: # pragma: no cover
586586
@winter.route_get('/method_return_1/')
587587
def method_return_1(self) -> Dataclass:
@@ -936,6 +936,99 @@ def method_request_body_undefined_2(self, data: DataclassWithUndefined):
936936
}
937937

938938

939+
def test_reuse_page_schema():
940+
class _TestAPI: # pragma: no cover
941+
@winter.route_get('/method_1/')
942+
def method_1(self) -> Page[str]:
943+
pass
944+
945+
@winter.route_get('/method_2/')
946+
def method_2(self) -> Page[str]:
947+
pass
948+
949+
result = generate_openapi(
950+
title='title',
951+
version='1.0.0',
952+
routes=[
953+
get_route(_TestAPI.method_1),
954+
get_route(_TestAPI.method_2),
955+
],
956+
)
957+
assert result == {
958+
'components': {
959+
'parameters': {},
960+
'responses': {},
961+
'schemas': {
962+
'PageMetaOfString': {
963+
'properties': {
964+
'limit': {'format': 'int32', 'nullable': True, 'type': 'integer'},
965+
'next': {'nullable': True, 'type': 'string'},
966+
'offset': {'format': 'int32', 'nullable': True, 'type': 'integer'},
967+
'previous': {'nullable': True, 'type': 'string'},
968+
'total_count': {'format': 'int32', 'type': 'integer'},
969+
},
970+
'required': ['total_count', 'limit', 'offset', 'previous', 'next'],
971+
'title': 'PageMetaOfString',
972+
'type': 'object'},
973+
'PageOfString': {
974+
'properties': {
975+
'meta': {'$ref': '#/components/schemas/PageMetaOfString'},
976+
'objects': {
977+
'items': {'type': 'string'},
978+
'type': 'array'
979+
}
980+
},
981+
'required': ['meta', 'objects'],
982+
'title': 'PageOfString',
983+
'type': 'object',
984+
},
985+
},
986+
},
987+
'info': {'title': 'title', 'version': '1.0.0'},
988+
'openapi': '3.0.3',
989+
'paths': {
990+
'/method_1/': {
991+
'get': {
992+
'deprecated': False,
993+
'operationId': '_TestAPI.method_1',
994+
'parameters': [],
995+
'responses': {
996+
'200': {
997+
'content': {
998+
'application/json': {
999+
'schema': {'$ref': '#/components/schemas/PageOfString'},
1000+
},
1001+
},
1002+
'description': '',
1003+
},
1004+
},
1005+
'tags': ['method_1'],
1006+
},
1007+
},
1008+
'/method_2/': {
1009+
'get': {
1010+
'deprecated': False,
1011+
'operationId': '_TestAPI.method_2',
1012+
'parameters': [],
1013+
'responses': {
1014+
'200': {
1015+
'content': {
1016+
'application/json': {
1017+
'schema': {'$ref': '#/components/schemas/PageOfString'},
1018+
},
1019+
},
1020+
'description': '',
1021+
},
1022+
},
1023+
'tags': ['method_2'],
1024+
},
1025+
},
1026+
},
1027+
'servers': [{'url': '/'}],
1028+
'tags': [],
1029+
}
1030+
1031+
9391032
def test_raises_for_type_duplicates():
9401033
class DuplicateTypes:
9411034
@dataclass

winter_openapi/inspectors/page_inspector.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import dataclasses
2+
from typing import Dict
23
from typing import List
34
from typing import Optional
5+
from typing import Type
46

57
from winter.data.pagination import Page
68
from winter_openapi.inspection.type_info import TypeInfo
79
from winter_openapi.inspectors.standard_types_inspectors import inspect_type
810
from winter_openapi.inspectors.standard_types_inspectors import register_type_inspector
911

1012

11-
# noinspection PyUnusedLocal
12-
@register_type_inspector(Page)
13-
def inspect_page(hint_class) -> TypeInfo:
14-
args = getattr(hint_class, '__args__', None)
13+
def create_dataclass(page_type: Type) -> Type:
14+
args = getattr(page_type, '__args__', None)
1515
child_class = args[0] if args else str
16-
extra_fields = set(dataclasses.fields(hint_class.__origin__)) - set(dataclasses.fields(Page))
16+
extra_fields = set(dataclasses.fields(page_type.__origin__)) - set(dataclasses.fields(Page))
1717
child_type_info = inspect_type(child_class)
1818
title = child_type_info.title or child_type_info.type_.capitalize()
1919

@@ -47,5 +47,16 @@ def inspect_page(hint_class) -> TypeInfo:
4747
),
4848
)
4949
PageDataclass.__doc__ = ''
50+
return PageDataclass
51+
52+
53+
page_to_dataclass_map: Dict[Type, Type] = {}
5054

51-
return inspect_type(PageDataclass)
55+
56+
# noinspection PyUnusedLocal
57+
@register_type_inspector(Page)
58+
def inspect_page(hint_class) -> TypeInfo:
59+
if hint_class not in page_to_dataclass_map:
60+
page_to_dataclass_map[hint_class] = create_dataclass(hint_class)
61+
page_dataclass = page_to_dataclass_map[hint_class]
62+
return inspect_type(page_dataclass)

0 commit comments

Comments
 (0)