diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 607105b0..88ed0102 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -7,18 +7,9 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.8, 3.9, 3.10.2, 3.11, 3.12] - sqlalchemy-version: [1.3, 1.4] - django-version: [2.2, 4.2] - exclude: - - python-version: '3.8' - sqlalchemy-version: '1.4' - - python-version: '3.9' - sqlalchemy-version: '1.4' - - python-version: '3.8' - django-version: '4.2' - - python-version: '3.9' - django-version: '4.2' + python-version: [3.11, 3.12, 3.13] + sqlalchemy-version: [ 1.4 ] + django-version: [ 4.2 ] name: Test Py ${{ matrix.python-version }}, SQLA ${{ matrix.sqlalchemy-version }}, Django ${{ matrix.django-version }} steps: - uses: actions/checkout@v3 @@ -58,7 +49,7 @@ jobs: python-version: 3.12 - name: Install dependencies run: | - pip3 install coverage==7.2.3 + pip3 install coverage==7.6.12 - name: Download coverage reports uses: actions/download-artifact@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 90076bba..4d3facf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ 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). +## [30.0.0] - 2025-02-27 +- Dropped support of Python earlier than 3.11 +- Dropped support of Django earlier than 4.2 +- Dropped support of SQLAlchemy earlier than 1.4 +- Added support of Python 3.13 +- Reviewed new typing-related things since Python 3.8: + - Added support of `Union[A, B]` → `A | B` + - Added support of `List[T]` → `list[T]`, `Dict[K, V]` → `dict[K, V]`, etc. + - Reviewed `Never` and `NoReturn` types - they are only for forever-looping functions, no sense to add their support + - Reviewed `Self` return type support - not suitable for endpoints, they do not return classes + - Added test for `TypeAlias` + - Reviewed `TypeGuard` - not suitable for endpoints + - Reviewed `LiteralString` - not suitable for endpoints, they usually return strings + - Reviewed `Concatenate` - not suitable for endpoints + - Added test for `TypedDict` +- Added tests of new typing features for event system + ## [29.0.2] - 2025-02-07 - Add support of UnionType in OpenAPI schema diff --git a/pyproject.toml b/pyproject.toml index 62fa3480..3fd88295 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "winter" -version = "29.0.2" +version = "30.0.0" homepage = "https://github.com/WinterFramework/winter" description = "Web Framework with focus on python typing, dataclasses and modular design" authors = ["Alexander Egorov "] @@ -11,14 +11,10 @@ classifiers = [ 'Operating System :: OS Independent', 'Environment :: Web Environment', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3', 'Framework :: Django :: 4', 'Topic :: Software Development :: Libraries :: Application Frameworks', ] @@ -31,13 +27,13 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.8" -Django = ">=2" +python = "^3.11" +Django = ">=4.2,<5" docstring-parser = ">=0.1" furl = ">=2.0.0, <3" python-dateutil = "^2.8.2" injector = ">=0.15.0, <1" -SQLAlchemy = ">=1.3, <2" +SQLAlchemy = ">=1.4, <2" typing-extensions = "^4.8" StrEnum = "^0.4.8" openapi-pydantic = ">=0.5.0, <0.6" @@ -51,11 +47,11 @@ flake8 = ">=3.7.7, <4" flake8-commas = ">=2.0.0, <4" flake8-formatter-abspath = ">=1.0.1, <2" pre-commit-hooks = ">=2.2.3, <3" -freezegun = ">=0.3.15, <1" +freezegun = ">=1.5.1, <2" mock = ">=2.0.0, <3" pytest = ">=6.2.5, <7" pytest-pythonpath = ">=0.7.1" -pytest-cov = ">=2.5.1, <3" +pytest-cov = "^6.0.0" pytest-django = ">=3.2.0, <4" semver = ">=2.8.1, <3" add-trailing-comma = ">=1.3.0, <2" diff --git a/tests/api/api_with_query_parameters.py b/tests/api/api_with_query_parameters.py index 01777779..8542ec21 100644 --- a/tests/api/api_with_query_parameters.py +++ b/tests/api/api_with_query_parameters.py @@ -1,36 +1,43 @@ import datetime from typing import Any - -from typing import Dict from typing import List from typing import Optional +from typing import TypeAlias from uuid import UUID import winter +ArrayAlias: TypeAlias = list[int] + @winter.route('with-query-parameter') class APIWithQueryParameters: @winter.map_query_parameter('string', to='mapped_string') - @winter.route_get('/{?date,boolean,optional_boolean,date_time,array,expanded_array*,string,uid}') + @winter.route_get('/{?date,boolean,optional_boolean,optional_boolean_new_typing_style,date_time,array,array_new_typing_style,array_alias,expanded_array*,string,uid}') def root( self, date: datetime.date, date_time: datetime.datetime, boolean: bool, array: List[int], + array_new_typing_style: list[int], + array_alias: ArrayAlias, expanded_array: List[str], mapped_string: str, uid: UUID, optional_boolean: Optional[bool] = None, - ) -> Dict[str, Any]: + optional_boolean_new_typing_style: bool | None = None, + ) -> dict[str, Any]: return { 'date': date, 'date_time': date_time, 'boolean': boolean, 'optional_boolean': optional_boolean, + 'optional_boolean_new_typing_style': optional_boolean_new_typing_style, 'array': array, + 'array_new_typing_style': array_new_typing_style, + 'array_alias': array_alias, 'expanded_array': expanded_array, 'string': mapped_string, 'uid': str(uid), diff --git a/tests/api/api_with_request_data.py b/tests/api/api_with_request_data.py index a323c494..bc4775b8 100644 --- a/tests/api/api_with_request_data.py +++ b/tests/api/api_with_request_data.py @@ -1,8 +1,12 @@ import enum from typing import List +from typing import NotRequired from typing import Optional import dataclasses +from typing import Required +from typing import TypeAlias +from typing import TypedDict import winter @@ -12,15 +16,28 @@ class Status(enum.Enum): SUPER_ACTIVE = 'super_active' +ItemsTypeAlias: TypeAlias = list[int] + + +class TypedDictExample(TypedDict): + field: str + required_field: Required[int] + optional_field: NotRequired[int] + + @dataclasses.dataclass class Data: id: int name: str is_god: bool optional_status: Optional[Status] + optional_status_new_typing_style: Status | None status: Status items: List[int] + items_alias: ItemsTypeAlias optional_items: Optional[List[int]] + optional_items_new_typing_style: list[int] | None + typed_dict: TypedDictExample with_default: int = 5 diff --git a/tests/core/json/test_decoder.py b/tests/core/json/test_decoder.py index 86f67d2f..47a396dc 100644 --- a/tests/core/json/test_decoder.py +++ b/tests/core/json/test_decoder.py @@ -58,6 +58,7 @@ class User: emails: List[str] = dataclasses.field(default_factory=list) created_at: Optional[datetime.datetime] = None name: str = 'test name' + comments: list[str] | None = None @dataclasses.dataclass(frozen=True) @@ -79,6 +80,7 @@ class Profile: 'contact': { 'phones': ['123', '456'], }, + 'comments': ['comment1', 'comment2'], }, User( Id(1), @@ -89,6 +91,7 @@ class Profile: ['test@test.ru'], datetime.datetime(year=2001, month=2, day=3, hour=4, minute=5, second=6, tzinfo=tzutc()), 'name', + ['comment1', 'comment2'], ), ), ( @@ -127,7 +130,9 @@ def test_decode(data, expected_instance): @pytest.mark.parametrize( ('data', 'type_', 'expected_instance'), ( (['super'], Set[Status], {Status.SUPER}), + (['super'], set[Status], {Status.SUPER}), ([1], Set, {1}), + ([1], set, {1}), ), ) def test_decode_set(data, type_, expected_instance): @@ -150,6 +155,8 @@ def test_decode_sequence(data, type_, expected_instance): ('data', 'type_', 'expected_instance'), ( (None, Optional[Status], None), ('super', Optional[Status], Status.SUPER), + (None, Status | None, None), + ('super', Status | None, Status.SUPER), ), ) def test_decode_optional(data, type_, expected_instance): @@ -180,6 +187,7 @@ def test_decode_set_with_errors(data, type_, expected_errors): (['super'], List[Status], [Status.SUPER]), (['1'], List[IntStatus], [IntStatus.SUPER]), ([1], List[IntStatus], [IntStatus.SUPER]), + ([1], list[IntStatus], [IntStatus.SUPER]), ({1}, List, [1]), ({1}, list, [1]), ), @@ -198,6 +206,7 @@ def test_decode_list(data, type_, expected_instance): ), (1, List[Status], 'Cannot decode "1" to list'), (None, List[Status], 'Cannot decode "None" to list'), + (None, list[Status], 'Cannot decode "None" to list'), (None, List[IntStatus], 'Cannot decode "None" to list'), (['a'], List[IntStatus], 'Value not in allowed values("1", "2"): "a"'), ), @@ -465,6 +474,11 @@ class DataclassWithUndefinedType: a: Union[int, Undefined] +@dataclasses.dataclass +class DataclassWithUndefinedTypeNewTypingStyle: + a: int | Undefined + + @dataclasses.dataclass class DataclassWithUndefinedByDefault: a: Union[int, Undefined] = Undefined() @@ -474,6 +488,8 @@ class DataclassWithUndefinedByDefault: ('data', 'type_', 'expected_result'), ( ({}, DataclassWithUndefinedType, DataclassWithUndefinedType(a=Undefined())), ({'a': 123}, DataclassWithUndefinedType, DataclassWithUndefinedType(a=123)), + ({}, DataclassWithUndefinedTypeNewTypingStyle, DataclassWithUndefinedTypeNewTypingStyle(a=Undefined())), + ({'a': 123}, DataclassWithUndefinedTypeNewTypingStyle, DataclassWithUndefinedTypeNewTypingStyle(a=123)), ({}, DataclassWithUndefinedByDefault, DataclassWithUndefinedByDefault(a=Undefined())), ({'a': 123}, DataclassWithUndefinedByDefault, DataclassWithUndefinedByDefault(a=123)), ), diff --git a/tests/messaging/test_simple_event_publisher.py b/tests/messaging/test_simple_event_publisher.py index 8b7a49ae..074b235b 100644 --- a/tests/messaging/test_simple_event_publisher.py +++ b/tests/messaging/test_simple_event_publisher.py @@ -1,7 +1,6 @@ from typing import List from typing import Union -from dataclasses import dataclass from injector import ClassProvider from injector import singleton @@ -69,6 +68,12 @@ def handler8(self, events: List[Union[Event1, Event3]]): self.result['handlers'].append(8) self.result['x'] += event.x + @event_handler + def handler9(self, events: list[Event1 | Event3]): + for event in events: + self.result['handlers'].append(9) + self.result['x'] += event.x + def test_simple_event_publisher(): injector = get_injector() @@ -87,23 +92,23 @@ def test_simple_event_publisher(): # Assert # emit event result = event_handlers.result - assert result == {'handlers': [1, 2, 7, 8], 'x': 40} + assert result == {'handlers': [1, 2, 7, 8, 9], 'x': 50} # emit same event again simple_event_publisher.emit(Event1(20)) - assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8], 'x': 120} + assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9], 'x': 150} # emit event with no handlers simple_event_publisher.emit(Event2(100)) - assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8], 'x': 120} + assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9], 'x': 150} # emit event with other annotation simple_event_publisher.emit(Event3(200)) - assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8, 3, 7, 8], 'x': 720} + assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9, 3, 7, 8, 9], 'x': 950} # emit event with handler for single event and list of events simple_event_publisher.emit(Event4(1000)) - assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8, 3, 7, 8, 5, 6], 'x': 2720} + assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9, 3, 7, 8, 9, 5, 6], 'x': 2950} # Test emit_many event_handlers.result = {'handlers': [], 'x': 0} @@ -111,13 +116,36 @@ def test_simple_event_publisher(): # emit event simple_event_publisher.emit_many([Event1(10)]) result = event_handlers.result - assert result == {'handlers': [1, 2, 7, 8], 'x': 40} + assert result == {'handlers': [1, 2, 7, 8, 9], 'x': 50} # emit same event twice simple_event_publisher.emit_many([Event3(200), Event3(200)]) - assert result == {'handlers': [1, 2, 7, 8, 3, 3, 7, 7, 8, 8], 'x': 1240} + assert result == {'handlers': [1, 2, 7, 8, 9, 3, 3, 7, 7, 8, 8, 9, 9], 'x': 1650} # emit different events simple_event_publisher.emit_many([Event1(10), Event2(100), Event3(200), Event4(1000)]) - assert result == {'handlers': [1, 2, 7, 8, 3, 3, 7, 7, 8, 8, 1, 2, 7, 7, 8, 8, 3, 5, 6], 'x': 3880} - + assert result == { + 'handlers': [1, 2, + 7, + 8, + 9, + 3, + 3, + 7, + 7, + 8, + 8, + 9, + 9, + 1, + 2, + 7, + 7, + 8, + 8, + 9, + 9, + 3, + 5, + 6], 'x': 4500 + } diff --git a/tests/routing/test_query_parameters.py b/tests/routing/test_query_parameters.py index 884b6d91..86e43833 100644 --- a/tests/routing/test_query_parameters.py +++ b/tests/routing/test_query_parameters.py @@ -183,20 +183,25 @@ def test_query_parameter(api_client, date, date_time, boolean, optional_boolean, 'date_time': '2019-05-01T22:28:31', 'boolean': boolean == 'true', 'optional_boolean': optional_boolean == 'true' if optional_boolean is not None else None, + 'optional_boolean_new_typing_style': optional_boolean == 'true' if optional_boolean is not None else None, 'array': array, + 'array_new_typing_style': array, + 'array_alias': array, 'expanded_array': list(map(str, array)), 'string': string, 'uid': str(uid), } base_uri = URITemplate( '/with-query-parameter/' - '{?date,date_time,boolean,optional_boolean,array,expanded_array*,string,uid}', + '{?date,date_time,boolean,optional_boolean,optional_boolean_new_typing_style,array,array_new_typing_style,array_alias,expanded_array*,string,uid}', ) query_params = { 'date': date, 'date_time': date_time, 'boolean': boolean, 'array': ','.join(map(str, array)), + 'array_new_typing_style': ','.join(map(str, array)), + 'array_alias': ','.join(map(str, array)), 'expanded_array': array, 'string': string, 'uid': uid, @@ -204,6 +209,7 @@ def test_query_parameter(api_client, date, date_time, boolean, optional_boolean, if optional_boolean is not None: query_params['optional_boolean'] = optional_boolean + query_params['optional_boolean_new_typing_style'] = optional_boolean base_uri = base_uri.expand(**query_params) @@ -215,13 +221,15 @@ def test_query_parameter(api_client, date, date_time, boolean, optional_boolean, def test_invalid_uuid_query_parameter_triggers_400(api_client): base_uri = URITemplate( '/with-query-parameter/' - '{?date,date_time,boolean,optional_boolean,array,expanded_array*,string,uid}', + '{?date,date_time,boolean,optional_boolean,array,array_new_typing_style,array_alias,expanded_array*,string,uid}', ) query_params = { 'date': datetime.datetime.now().date(), 'date_time': datetime.datetime.now(), 'boolean': 'true', 'array': '5', + 'array_new_typing_style': '5', + 'array_alias': '5', 'expanded_array': ['5'], 'string': '', 'uid': str(uuid4()) + 'a', diff --git a/tests/test_type_utils.py b/tests/test_type_utils.py index 548f8556..12f9cfd9 100644 --- a/tests/test_type_utils.py +++ b/tests/test_type_utils.py @@ -1,6 +1,5 @@ -import sys - from collections.abc import Iterable +from types import UnionType from typing import Dict from typing import List from typing import Optional @@ -8,6 +7,7 @@ from typing import Union import pytest + from winter.core.utils.typing import get_origin_type from winter.core.utils.typing import get_type_name from winter.core.utils.typing import is_iterable_type @@ -20,6 +20,8 @@ ('typing_for_check', 'expected'), [ (Optional[List[int]], True), (Union[List[int], Tuple], True), + (Union[List | Tuple], True), + (Union[list | tuple], True), (int, False), (str, False), (Optional[List[int]], True), @@ -34,6 +36,7 @@ def test_is_iterable_type(typing_for_check, expected): ('typing_for_check', 'expected'), [ (Optional[int], True), (Union[List, Tuple], True), + (Union[list, tuple], True), (Tuple, False), (Union, True), ], @@ -46,7 +49,9 @@ def test_is_union(typing_for_check, expected): ('typing_for_check', 'expected'), [ (Optional[int], True), (Union[int, None], True), + (int | None, True), (Union[int, list], False), + (int | list, False), (int, False), (Union, False), ], @@ -60,6 +65,7 @@ def test_is_optional(typing_for_check, expected): (int, int), (Union[int], int), (Union[int, float], Union), + (int | float, UnionType), ], ) def test_get_origin_type(typing_for_check, expected): @@ -71,6 +77,7 @@ def test_get_origin_type(typing_for_check, expected): (Union[int], int, True), (List, Iterable, True), (List[int], Iterable, True), + (list[int], Iterable, True), ], ) def test_is_origin_type_subclasses(typing_for_check, subclasses_type, expected): @@ -82,11 +89,15 @@ def test_is_origin_type_subclasses(typing_for_check, subclasses_type, expected): @pytest.mark.parametrize( ('type_', 'expected_type_name'), [ (int, 'int'), - (Dict[int, str], 'Dict[int, str]' if sys.version_info >= (3, 7, 0) else 'Dict'), - (Tuple[int], 'Tuple[int]' if sys.version_info >= (3, 7, 0) else 'Tuple'), - (List[int], 'List[int]' if sys.version_info >= (3, 7, 0) else 'List'), + (Dict[int, str], 'Dict[int, str]'), + (Tuple[int], 'Tuple[int]'), + (List[int], 'List[int]'), (Optional[str], 'Optional[str]'), (Union[int, str], 'Union[int, str]'), + (dict[int, str], 'dict[int, str]'), + (tuple[int], 'tuple[int]'), + (list[int], 'list[int]'), + (int | str, 'int | str'), (1, 'int'), ], ) diff --git a/tests/web/test_request_body.py b/tests/web/test_request_body.py index 19b65e75..60d14f04 100644 --- a/tests/web/test_request_body.py +++ b/tests/web/test_request_body.py @@ -12,6 +12,12 @@ def test_request_body(api_client): 'is_god': True, 'status': 'active', 'items': [1, 2], + 'items_alias': [1, 2], + 'typed_dict': { + 'field': 'field', + 'required_field': 1, + 'optional_field': 2, + }, } expected_data = { 'id': 1, @@ -20,8 +26,16 @@ def test_request_body(api_client): 'is_god': True, 'status': 'active', 'optional_status': None, + 'optional_status_new_typing_style': None, 'items': [1, 2], + 'items_alias': [1, 2], 'optional_items': None, + 'optional_items_new_typing_style': None, + 'typed_dict': { + 'field': 'field', + 'required_field': 1, + 'optional_field': 2, + }, } # Act @@ -38,6 +52,12 @@ def test_request_body_as_list(api_client): 'is_god': True, 'status': 'active', 'items': [1, 2], + 'items_alias': [1, 2], + 'typed_dict': { + 'field': 'field', + 'required_field': 1, + 'optional_field': 2, + }, }] expected_data = [{ 'id': 1, @@ -46,8 +66,16 @@ def test_request_body_as_list(api_client): 'is_god': True, 'status': 'active', 'optional_status': None, + 'optional_status_new_typing_style': None, 'items': [1, 2], + 'items_alias': [1, 2], 'optional_items': None, + 'optional_items_new_typing_style': None, + 'typed_dict': { + 'field': 'field', + 'required_field': 1, + 'optional_field': 2, + }, }] # Act @@ -64,6 +92,12 @@ def test_request_body_with_errors(api_client): 'status': 'invalid status', 'invalid_key': 'data', 'items': ['invalid integer'], + 'items_alias': ['invalid integer'], + 'typed_dict': { + 'field': 'field', + 'required_field': 1, + 'optional_field': 2, + }, } expected_data = { @@ -75,6 +109,7 @@ def test_request_body_with_errors(api_client): 'id': 'Cannot decode "invalid integer" to integer', 'status': 'Value not in allowed values("active", "super_active"): "invalid status"', 'items': 'Cannot decode "invalid integer" to integer', + 'items_alias': 'Cannot decode "invalid integer" to integer', 'non_field_error': 'Missing fields: "name"', } } diff --git a/tests/winter_ddd/test_process_domain_events.py b/tests/winter_ddd/test_process_domain_events.py index ed434c86..2f351411 100644 --- a/tests/winter_ddd/test_process_domain_events.py +++ b/tests/winter_ddd/test_process_domain_events.py @@ -28,6 +28,7 @@ class _TestHandler: handled_many_another_domain_events = [] handled_union_domain_events = [] handled_union_list_domain_events = [] + handled_union_list_domain_events_new_typing_style = [] @domain_event_handler def empty_handle(self, domain_event: CustomDomainEvent): @@ -53,6 +54,10 @@ def handle_union(self, domain_event: Union[CustomDomainEvent, AnotherCustomEvent def handle_union_list(self, domain_events: List[Union[CustomDomainEvent, AnotherCustomEvent]]): self.handled_union_list_domain_events.append(domain_events) + @domain_event_handler + def handle_union_list_new_typing_style(self, domain_events: list[CustomDomainEvent | AnotherCustomEvent]): + self.handled_union_list_domain_events_new_typing_style.append(domain_events) + class DomainEventForOrder(DomainEvent): pass @@ -122,6 +127,7 @@ def test_process_domain_events(): assert _TestHandler.handled_many_another_domain_events == [[another_domain_event]] assert _TestHandler.handled_union_domain_events == [domain_event1, another_domain_event, domain_event2] assert _TestHandler.handled_union_list_domain_events == [[domain_event1, another_domain_event, domain_event2]] + assert _TestHandler.handled_union_list_domain_events_new_typing_style == [[domain_event1, another_domain_event, domain_event2]] def test_order_process_domain_events(): diff --git a/tests/winter_openapi/test_api_request_and_response_spec.py b/tests/winter_openapi/test_api_request_and_response_spec.py index fef08593..01c518f6 100644 --- a/tests/winter_openapi/test_api_request_and_response_spec.py +++ b/tests/winter_openapi/test_api_request_and_response_spec.py @@ -575,10 +575,6 @@ def simple_method(self, data: type_hint): # pragma: no cover assert result['components'] == expected_components -@pytest.mark.skipif( - not sys.version_info >= (3, 10), - reason="These tests require Python 3.10 or higher" -) def test_request_type_with_union_undefined(): @dataclass class RequestBodyWithUnionUndefined: diff --git a/winter/core/utils/typing.py b/winter/core/utils/typing.py index 2ed09ede..a37688ce 100644 --- a/winter/core/utils/typing.py +++ b/winter/core/utils/typing.py @@ -3,10 +3,9 @@ from typing import Iterable from typing import TypeVar from typing import Union -from typing import get_args +from typing import get_origin NoneType = type(None) -UnionType = type(Union) def is_optional(type_: object) -> bool: @@ -30,7 +29,7 @@ def is_iterable_type(type_: object) -> bool: def is_union(type_: object) -> bool: - return get_origin_type(type_) == Union + return get_origin_type(type_) in (Union, types.UnionType) def get_union_args(type_: object) -> list: @@ -38,13 +37,7 @@ def get_union_args(type_: object) -> list: def get_origin_type(hint_class): - if hasattr(types, 'UnionType') and isinstance(hint_class, types.UnionType): - # Extract the arguments of the union (e.g., `str | int` -> (str, int)) - args = get_args(hint_class) - # Convert to the old `typing.Union` style - hint_class = Union[args] - - return getattr(hint_class, '__origin__', None) or hint_class + return get_origin(hint_class) or hint_class def is_origin_type_subclasses(hint_class, check_class): @@ -66,4 +59,7 @@ def get_type_name(type_): type_name = type_name[7:] return type_name + if type(type_) in (types.GenericAlias, types.UnionType): + return type_name + return type(type_).__name__ diff --git a/winter_openapi/inspectors/standard_types_inspectors.py b/winter_openapi/inspectors/standard_types_inspectors.py index dd101bc1..1770bf29 100644 --- a/winter_openapi/inspectors/standard_types_inspectors.py +++ b/winter_openapi/inspectors/standard_types_inspectors.py @@ -172,14 +172,6 @@ def inspect_type_wrapper(hint_class) -> TypeInfo: return schema -@register_type_inspector( - types.FunctionType, - checker=lambda instance: getattr(instance, '__supertype__', None) is not None, -) -def inspect_new_type(hint_class) -> TypeInfo: - return inspect_type(hint_class.__supertype__) - - @register_type_inspector( typing.NewType, checker=lambda instance: getattr(instance, '__supertype__', None) is not None,