diff --git a/.idea/combadge.iml b/.idea/combadge.iml index d34ef58..0e6c152 100644 --- a/.idea/combadge.iml +++ b/.idea/combadge.iml @@ -2,8 +2,8 @@ - + @@ -12,7 +12,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 36d0c47..b1f658c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/combadge/support/http/markers/request.py b/combadge/support/http/markers/request.py index 50aec29..3b01ebf 100644 --- a/combadge/support/http/markers/request.py +++ b/combadge/support/http/markers/request.py @@ -143,55 +143,37 @@ def __call__(self, request: ContainsQueryParams, value: Any) -> None: # noqa: D request.query_params.append((self.name, sub_value.value if isinstance(sub_value, Enum) else sub_value)) -if not TYPE_CHECKING: - - @dataclass(**SLOTS) - class Payload(ParameterMarker[ContainsPayload]): - """ - Mark parameter as a request payload. - - An argument gets converted to a dictionary and passed over to a backend. - - Examples: - Simple usage: - - >>> def call(body: Payload[BodyModel]) -> ...: - >>> ... - - Equivalent expanded usage: +@dataclass(**SLOTS) +class Payload(ParameterMarker[ContainsPayload]): + """ + Mark parameter as a request payload. - >>> def call(body: Annotated[BodyModel, Payload()]) -> ...: - >>> ... - """ + An argument gets converted to a dictionary and passed over to a backend. - exclude_unset: bool = False - by_alias: bool = False + Examples: + >>> def call(body: Annotated[BodyModel, Payload()]) -> ...: + >>> ... + """ - @override - def __call__(self, request: ContainsPayload, value: Any) -> None: # noqa: D102 - value = get_type_adapter(type(value)).dump_python( - value, - by_alias=self.by_alias, - exclude_unset=self.exclude_unset, - ) - if request.payload is None: - request.payload = value - elif isinstance(request.payload, dict): - request.payload.update(value) # merge into the existing payload - else: - raise ValueError(f"attempting to merge `{type(value)}` into `{type(request.payload)}`") + exclude_unset: bool = False + by_alias: bool = False - def __class_getitem__(cls, item: type[Any]) -> Any: - return Annotated[item, cls()] + @override + def __call__(self, request: ContainsPayload, value: Any) -> None: # noqa: D102 + value = get_type_adapter(type(value)).dump_python( + value, + by_alias=self.by_alias, + exclude_unset=self.exclude_unset, + ) + if request.payload is None: + request.payload = value + elif isinstance(request.payload, dict): + request.payload.update(value) # merge into the existing payload + else: + raise ValueError(f"attempting to merge `{type(value)}` into `{type(request.payload)}`") -else: - # Abandon hope all ye who enter here 👋 - # - # Mypy still does not support `__class_getitem__`, although it was introduced in Python 3.7: - # https://github.com/python/mypy/issues/11501. - # This line allows to treat `Payload[T]` simply as `T` itself, that is consistent - # with `Annotated[T, Payload()]` annotation. - Payload: TypeAlias = _T + def __class_getitem__(cls, item: type[Any]) -> Any: + raise NotImplementedError("the shortcut is no longer supported, use `Annotated[..., Payload()]`") @dataclass(**SLOTS) @@ -216,36 +198,28 @@ def __call__(self, request: ContainsPayload, value: Any) -> None: # noqa: D102 request.payload[self.name] = value.value if isinstance(value, Enum) else value -if not TYPE_CHECKING: - - @dataclass(**SLOTS) - class FormData(ParameterMarker[ContainsFormData]): - """ - Mark parameter as a request form data. - - An argument gets converted to a dictionary and passed over to a backend. - - Examples: - >>> def call(body: FormData[FormModel]) -> ...: - >>> ... - - >>> def call(body: Annotated[FormModel, FormData()]) -> ...: - >>> ... - """ +@dataclass(**SLOTS) +class FormData(ParameterMarker[ContainsFormData]): + """ + Mark parameter as a request form data. - @override - def __call__(self, request: ContainsFormData, value: Any) -> None: # noqa: D102 - value = get_type_adapter(type(value)).dump_python(value, by_alias=True) - if not isinstance(value, dict): - raise TypeError(f"form data requires a dictionary, got {type(value)}") - for item_name, item_value in value.items(): - request.append_form_field(item_name, item_value) + An argument gets converted to a dictionary and passed over to a backend. - def __class_getitem__(cls, item: type[Any]) -> Any: - return Annotated[item, FormData()] + Examples: + >>> def call(body: Annotated[FormModel, FormData()]) -> ...: + >>> ... + """ -else: - FormData: TypeAlias = _T + @override + def __call__(self, request: ContainsFormData, value: Any) -> None: # noqa: D102 + value = get_type_adapter(type(value)).dump_python(value, by_alias=True) + if not isinstance(value, dict): + raise TypeError(f"form data requires a dictionary, got {type(value)}") + for item_name, item_value in value.items(): + request.append_form_field(item_name, item_value) + + def __class_getitem__(cls, item: type[Any]) -> Any: + raise NotImplementedError("the shortcut is no longer supported, use `Annotated[..., FormData()]`") @dataclass(**SLOTS) diff --git a/combadge/support/soap/markers.py b/combadge/support/soap/markers.py index 44a30b8..008cb11 100644 --- a/combadge/support/soap/markers.py +++ b/combadge/support/soap/markers.py @@ -2,7 +2,7 @@ from inspect import BoundArguments from typing import TYPE_CHECKING, Annotated, Any, Callable, Generic, TypeVar -from typing_extensions import TypeAlias, override +from typing_extensions import TypeAlias, override, TypeAliasType from combadge._helpers.dataclasses import SLOTS from combadge._helpers.pydantic import get_type_adapter @@ -39,46 +39,34 @@ def operation_name(name: str) -> Callable[[FunctionT], FunctionT]: return OperationName[Any](name).mark -if not TYPE_CHECKING: - - @dataclass(**SLOTS) - class Header(ParameterMarker[ContainsSoapHeader]): - """ - Mark parameter as a request header. - - An argument gets converted to a dictionary and passed over to a backend. - - Examples: - Simple usage: - - >>> def call(body: Header[HeaderModel]) -> ...: - >>> ... - - Equivalent expanded usage: - - >>> def call(body: Annotated[HeaderModel, Header()]) -> ...: - >>> ... - """ +@dataclass(**SLOTS) +class Header(ParameterMarker[ContainsSoapHeader]): + """ + Mark parameter as a request header. - exclude_unset: bool = False - by_alias: bool = False + An argument gets converted to a dictionary and passed over to a backend. - @override - def __call__(self, request: ContainsSoapHeader, value: Any) -> None: # noqa: D102 - value = get_type_adapter(type(value)).dump_python( - value, - by_alias=self.by_alias, - exclude_unset=self.exclude_unset, - ) - if request.soap_header is None: - request.soap_header = value - elif isinstance(request.soap_header, dict): - request.soap_header.update(value) # merge into the existing header - else: - raise ValueError(f"attempting to merge `{type(value)}` into `{type(request.soap_header)}`") + Examples: + >>> def call(body: Annotated[HeaderModel, Header()]) -> ...: + >>> ... + """ - def __class_getitem__(cls, item: type[Any]) -> Any: - return Annotated[item, cls()] + exclude_unset: bool = False + by_alias: bool = False -else: - FormData: TypeAlias = _T + @override + def __call__(self, request: ContainsSoapHeader, value: Any) -> None: # noqa: D102 + value = get_type_adapter(type(value)).dump_python( + value, + by_alias=self.by_alias, + exclude_unset=self.exclude_unset, + ) + if request.soap_header is None: + request.soap_header = value + elif isinstance(request.soap_header, dict): + request.soap_header.update(value) # merge into the existing header + else: + raise ValueError(f"attempting to merge `{type(value)}` into `{type(request.soap_header)}`") + + def __class_getitem__(cls, item: type[Any]) -> Any: + raise NotImplementedError("the shortcut is no longer supported, use `Annotated[..., Header()]`") diff --git a/docs/support/models/index.md b/docs/support/models/index.md index a47a85c..123a5e5 100644 --- a/docs/support/models/index.md +++ b/docs/support/models/index.md @@ -18,7 +18,7 @@ from httpx import Client class Httpbin(Protocol): @http_method("POST") @path("/anything") - def post_anything(self, foo: Payload[int]) -> Annotated[int, Extract("data")]: + def post_anything(self, foo: Annotated[int, Payload()]) -> Annotated[int, Extract("data")]: ... @@ -31,7 +31,7 @@ assert backend[Httpbin].post_anything(42) == 42 ```python title="dataclasses.py" hl_lines="10-12 15-17 23 28" from dataclasses import dataclass -from typing_extensions import Protocol +from typing_extensions import Protocol, Annotated from combadge.support.httpx.backends.sync import HttpxBackend from combadge.support.http.markers import Payload, http_method, path @@ -51,7 +51,7 @@ class Response: class Httpbin(Protocol): @http_method("POST") @path("/anything") - def post_anything(self, foo: Payload[Request]) -> Response: + def post_anything(self, foo: Annotated[Request, Payload()]) -> Response: ... @@ -62,7 +62,7 @@ assert backend[Httpbin].post_anything(Request(42)) == Response(data='{"foo": 42} ## [Typed dictionaries](https://docs.python.org/3/library/typing.html#typing.TypedDict) ```python title="typed_dict.py" hl_lines="8-9 12-13 19 24" -from typing_extensions import Protocol, TypedDict +from typing_extensions import Protocol, TypedDict, Annotated from combadge.support.httpx.backends.sync import HttpxBackend from combadge.support.http.markers import Payload, http_method, path @@ -80,7 +80,7 @@ class Response(TypedDict): class Httpbin(Protocol): @http_method("POST") @path("/anything") - def post_anything(self, foo: Payload[Request]) -> Response: + def post_anything(self, foo: Annotated[Request, Payload()]) -> Response: ... diff --git a/tests/core/markers/test_parameter.py b/tests/core/markers/test_parameter.py index e7ba328..d51ccf5 100644 --- a/tests/core/markers/test_parameter.py +++ b/tests/core/markers/test_parameter.py @@ -3,14 +3,13 @@ import pytest from combadge.core.markers.parameter import ParameterMarker -from combadge.support.http.markers import CustomHeader, Payload +from combadge.support.http.markers import CustomHeader @pytest.mark.parametrize( ("type_", "expected"), [ (int, []), - (Payload[int], [Payload()]), (Annotated[str, CustomHeader("X-Header")], [CustomHeader("X-Header")]), ], ) diff --git a/tests/integration/test_httpbin.py b/tests/integration/test_httpbin.py index 6bcf40a..4528581 100644 --- a/tests/integration/test_httpbin.py +++ b/tests/integration/test_httpbin.py @@ -36,7 +36,7 @@ class SupportsHttpbin(SupportsService, Protocol): @abstractmethod def post_anything( self, - data: FormData[Data], + data: Annotated[Data, FormData()], bar: Annotated[int, FormField("barqux")], qux: Annotated[int, FormField("barqux")], ) -> Response: ...