Skip to content

Commit

Permalink
BREAK: remove shortcuts Payload[...], Header[...], and `FormData[…
Browse files Browse the repository at this point in the history
…...]` 💥

MyPy is having a hard time properly supporting generic classes with overridden `__class_getitem__()`, so having these shortcuts creates more problems than it solves. I'm removing the overrides for the time being, until MyPy implements a proper support for them.
  • Loading branch information
eigenein committed Nov 28, 2024
1 parent 528a91c commit 99bd0b1
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 122 deletions.
4 changes: 2 additions & 2 deletions .idea/combadge.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 45 additions & 71 deletions combadge/support/http/markers/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 171 in combadge/support/http/markers/request.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/http/markers/request.py#L171

Added line #L171 was not covered by tests
else:
raise ValueError(f"attempting to merge `{type(value)}` into `{type(request.payload)}`")

Check warning on line 173 in combadge/support/http/markers/request.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/http/markers/request.py#L173

Added line #L173 was not covered by tests

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)
Expand All @@ -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)}")

Check warning on line 217 in combadge/support/http/markers/request.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/http/markers/request.py#L217

Added line #L217 was not covered by tests
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)
Expand Down
68 changes: 28 additions & 40 deletions combadge/support/soap/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from inspect import BoundArguments
from typing import TYPE_CHECKING, Annotated, Any, Callable, Generic, TypeVar

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.11)

Ruff (F401)

combadge/support/soap/markers.py:3:20: F401 `typing.TYPE_CHECKING` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.11)

Ruff (F401)

combadge/support/soap/markers.py:3:35: F401 `typing.Annotated` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.9)

Ruff (F401)

combadge/support/soap/markers.py:3:20: F401 `typing.TYPE_CHECKING` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.9)

Ruff (F401)

combadge/support/soap/markers.py:3:35: F401 `typing.Annotated` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.10)

Ruff (F401)

combadge/support/soap/markers.py:3:20: F401 `typing.TYPE_CHECKING` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.10)

Ruff (F401)

combadge/support/soap/markers.py:3:35: F401 `typing.Annotated` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.13)

Ruff (F401)

combadge/support/soap/markers.py:3:20: F401 `typing.TYPE_CHECKING` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.13)

Ruff (F401)

combadge/support/soap/markers.py:3:35: F401 `typing.Annotated` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.12)

Ruff (F401)

combadge/support/soap/markers.py:3:20: F401 `typing.TYPE_CHECKING` imported but unused

Check failure on line 3 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.12)

Ruff (F401)

combadge/support/soap/markers.py:3:35: F401 `typing.Annotated` imported but unused

from typing_extensions import TypeAlias, override
from typing_extensions import TypeAlias, override, TypeAliasType

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.11)

Ruff (F401)

combadge/support/soap/markers.py:5:31: F401 `typing_extensions.TypeAlias` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.11)

Ruff (F401)

combadge/support/soap/markers.py:5:52: F401 `typing_extensions.TypeAliasType` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.9)

Ruff (F401)

combadge/support/soap/markers.py:5:31: F401 `typing_extensions.TypeAlias` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.9)

Ruff (F401)

combadge/support/soap/markers.py:5:52: F401 `typing_extensions.TypeAliasType` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.10)

Ruff (F401)

combadge/support/soap/markers.py:5:31: F401 `typing_extensions.TypeAlias` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.10)

Ruff (F401)

combadge/support/soap/markers.py:5:52: F401 `typing_extensions.TypeAliasType` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.13)

Ruff (F401)

combadge/support/soap/markers.py:5:31: F401 `typing_extensions.TypeAlias` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.13)

Ruff (F401)

combadge/support/soap/markers.py:5:52: F401 `typing_extensions.TypeAliasType` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.12)

Ruff (F401)

combadge/support/soap/markers.py:5:31: F401 `typing_extensions.TypeAlias` imported but unused

Check failure on line 5 in combadge/support/soap/markers.py

View workflow job for this annotation

GitHub Actions / Check (3.12)

Ruff (F401)

combadge/support/soap/markers.py:5:52: F401 `typing_extensions.TypeAliasType` imported but unused

from combadge._helpers.dataclasses import SLOTS
from combadge._helpers.pydantic import get_type_adapter
Expand Down Expand Up @@ -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(

Check warning on line 59 in combadge/support/soap/markers.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/soap/markers.py#L59

Added line #L59 was not covered by tests
value,
by_alias=self.by_alias,
exclude_unset=self.exclude_unset,
)
if request.soap_header is None:
request.soap_header = value

Check warning on line 65 in combadge/support/soap/markers.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/soap/markers.py#L65

Added line #L65 was not covered by tests
elif isinstance(request.soap_header, dict):
request.soap_header.update(value) # merge into the existing header

Check warning on line 67 in combadge/support/soap/markers.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/soap/markers.py#L67

Added line #L67 was not covered by tests
else:
raise ValueError(f"attempting to merge `{type(value)}` into `{type(request.soap_header)}`")

Check warning on line 69 in combadge/support/soap/markers.py

View check run for this annotation

Codecov / codecov/patch

combadge/support/soap/markers.py#L69

Added line #L69 was not covered by tests

def __class_getitem__(cls, item: type[Any]) -> Any:
raise NotImplementedError("the shortcut is no longer supported, use `Annotated[..., Header()]`")
10 changes: 5 additions & 5 deletions docs/support/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")]:
...


Expand All @@ -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
Expand All @@ -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:
...


Expand All @@ -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
Expand All @@ -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:
...


Expand Down
3 changes: 1 addition & 2 deletions tests/core/markers/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]),
],
)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_httpbin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down

0 comments on commit 99bd0b1

Please sign in to comment.