Skip to content

Commit

Permalink
FIX: restore the shortcuts Payload[...], Header[...], and `FormDa…
Browse files Browse the repository at this point in the history
…ta[...]` 🧑‍💻
  • Loading branch information
eigenein committed Jan 30, 2025
1 parent d157a5b commit 621f313
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 90 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:

- name: "✨ Install Poetry"
run: |
pipx install poetry
pipx install "poetry<2.0.0"
pipx inject poetry poetry-dynamic-versioning
- name: 🐍 Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
fetch-depth: 0

- name: "✨ Install Poetry"
run: pipx install poetry
run: pipx install "poetry<2.0.0"

- uses: actions/setup-python@v5
name: "✨ Set up Python"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

- name: "✨ Install Poetry"
run: |
pipx install poetry
pipx install "poetry<2.0.0"
pipx inject poetry poetry-dynamic-versioning
- name: 📦 Build package
Expand Down
2 changes: 2 additions & 0 deletions combadge/core/typevars.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
if TYPE_CHECKING:
from combadge.core.backend import BaseBackend

AnyT = TypeVar("AnyT")

BackendT = TypeVar("BackendT", bound="BaseBackend")
"""Backend type."""

Expand Down
110 changes: 62 additions & 48 deletions combadge/support/http/markers/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from dataclasses import dataclass
from enum import Enum
from inspect import BoundArguments
from typing import Any, Callable, Generic, cast
from typing import TYPE_CHECKING, Annotated, Any, Callable, Generic, cast

from annotated_types import SLOTS
from typing_extensions import override
from typing_extensions import TypeAlias, override

from combadge._helpers.pydantic import get_type_adapter
from combadge.core.markers.method import MethodMarker
from combadge.core.markers.parameter import ParameterMarker
from combadge.core.typevars import FunctionT
from combadge.core.typevars import AnyT, FunctionT
from combadge.support.http.abc import (
HttpRequestFormData,
HttpRequestHeaders,
Expand Down Expand Up @@ -142,37 +142,44 @@ def __call__(self, request: HttpRequestQueryParams, value: Any) -> None: # noqa
request.query_params.append((self.name, sub_value.value if isinstance(sub_value, Enum) else sub_value))


@dataclass(**SLOTS)
class Payload(ParameterMarker[HttpRequestPayload]):
"""
Mark parameter as a request payload.
if TYPE_CHECKING:
Payload: TypeAlias = Annotated[AnyT, ...]
else:

An argument gets converted to a dictionary and passed over to a backend.
@dataclass(**SLOTS)
class Payload(ParameterMarker[HttpRequestPayload]):
"""
Mark parameter as a request payload.
Examples:
>>> 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: HttpRequestPayload, value: Any) -> None: # noqa: D102
value = get_type_adapter(cast(Hashable, 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)}`")
>>> def call(body: Payload[BodyModel]) -> ...:
>>> ...
"""

exclude_unset: bool = False
by_alias: bool = False

@override
def __call__(self, request: HttpRequestPayload, value: Any) -> None: # noqa: D102
value = get_type_adapter(cast(Hashable, 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)}`")

def __class_getitem__(cls, item: type[Any]) -> Any:
raise NotImplementedError("the shortcut is no longer supported, use `Annotated[..., Payload()]`")
def __class_getitem__(cls, item: type[Any]) -> Any:
return Annotated[item, cls()]


@dataclass(**SLOTS)
Expand All @@ -197,28 +204,35 @@ def __call__(self, request: HttpRequestPayload, value: Any) -> None: # noqa: D1
request.payload[self.name] = value.value if isinstance(value, Enum) else value


@dataclass(**SLOTS)
class FormData(ParameterMarker[HttpRequestFormData]):
"""
Mark parameter as a request form data.
if TYPE_CHECKING:
FormData: TypeAlias = Annotated[AnyT, ...]
else:

An argument gets converted to a dictionary and passed over to a backend.
@dataclass(**SLOTS)
class FormData(ParameterMarker[HttpRequestFormData]):
"""
Mark parameter as a request form data.
Examples:
>>> def call(body: Annotated[FormModel, FormData()]) -> ...:
>>> ...
"""
An argument gets converted to a dictionary and passed over to a backend.
@override
def __call__(self, request: HttpRequestFormData, value: Any) -> None: # noqa: D102
value = get_type_adapter(cast(Hashable, 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()]`")
Examples:
>>> def call(body: Annotated[FormModel, FormData()]) -> ...:
>>> ...
>>> def call(body: FormData[FormModel]) -> ...:
>>> ...
"""

@override
def __call__(self, request: HttpRequestFormData, value: Any) -> None: # noqa: D102
value = get_type_adapter(cast(Hashable, 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:
return Annotated[item, cls()]


@dataclass(**SLOTS)
Expand Down
75 changes: 41 additions & 34 deletions combadge/support/soap/markers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from collections.abc import Hashable
from dataclasses import dataclass
from inspect import BoundArguments
from typing import Any, Callable, Generic, cast
from typing import TYPE_CHECKING, Annotated, Any, Callable, Generic, cast

from annotated_types import SLOTS
from typing_extensions import override
from typing_extensions import TypeAlias, override

from combadge._helpers.pydantic import get_type_adapter
from combadge.core.markers.method import MethodMarker
from combadge.core.markers.parameter import ParameterMarker
from combadge.core.typevars import FunctionT
from combadge.core.typevars import AnyT, FunctionT
from combadge.support.soap.abc import SoapHeader, SoapOperationName


Expand Down Expand Up @@ -38,34 +38,41 @@ def operation_name(name: str) -> Callable[[FunctionT], FunctionT]:
return OperationName[Any](name).mark


@dataclass(**SLOTS)
class Header(ParameterMarker[SoapHeader]):
"""
Mark parameter as a request header.
An argument gets converted to a dictionary and passed over to a backend.
Examples:
>>> def call(body: Annotated[HeaderModel, Header()]) -> ...:
>>> ...
"""

exclude_unset: bool = False
by_alias: bool = False

@override
def __call__(self, request: SoapHeader, value: Any) -> None: # noqa: D102
value = get_type_adapter(cast(Hashable, 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()]`")
if TYPE_CHECKING:
Header: TypeAlias = Annotated[AnyT, ...]
else:

@dataclass(**SLOTS)
class Header(ParameterMarker[SoapHeader]):
"""
Mark parameter as a request header.
An argument gets converted to a dictionary and passed over to a backend.
Examples:
>>> def call(body: Annotated[HeaderModel, Header()]) -> ...:
>>> ...
>>> def call(body: Header[HeaderModel]) -> ...:
>>> ...
"""

exclude_unset: bool = False
by_alias: bool = False

@override
def __call__(self, request: SoapHeader, value: Any) -> None: # noqa: D102
value = get_type_adapter(cast(Hashable, 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:
return Annotated[item, cls()]
6 changes: 3 additions & 3 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: Annotated[int, Payload()]) -> Annotated[int, Extract("data")]:
def post_anything(self, foo: Payload[int]) -> Annotated[int, Extract("data")]:
...


Expand Down Expand Up @@ -51,7 +51,7 @@ class Response:
class Httpbin(Protocol):
@http_method("POST")
@path("/anything")
def post_anything(self, foo: Annotated[Request, Payload()]) -> Response:
def post_anything(self, foo: Payload[int]) -> Response:
...


Expand Down Expand Up @@ -80,7 +80,7 @@ class Response(TypedDict):
class Httpbin(Protocol):
@http_method("POST")
@path("/anything")
def post_anything(self, foo: Annotated[Request, Payload()]) -> Response:
def post_anything(self, foo: Payload[Request]) -> Response:
...


Expand Down
3 changes: 2 additions & 1 deletion tests/core/markers/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import pytest

from combadge.core.markers.parameter import ParameterMarker
from combadge.support.http.markers import CustomHeader
from combadge.support.http.markers import CustomHeader, Payload


@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 @@ -35,7 +35,7 @@ class SupportsHttpbin(Protocol):
@abstractmethod
def post_anything(
self,
data: Annotated[Data, FormData()],
data: FormData[Data],
bar: Annotated[int, FormField("barqux")],
qux: Annotated[int, FormField("barqux")],
) -> Response: ...
Expand Down

0 comments on commit 621f313

Please sign in to comment.