Skip to content

Commit

Permalink
NEW: support callback protocols in the binder ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
eigenein committed Nov 13, 2024
1 parent cd48cd9 commit 3e3bd8b
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 8 deletions.
2 changes: 1 addition & 1 deletion combadge/core/binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _enumerate_methods(of_protocol: type) -> Iterable[tuple[str, Any]]:
"""Enumerate the service protocol methods."""

for name, method in get_members(of_protocol, callable):
if name.startswith("_"):
if name.startswith("_") and name != "__call__":
continue
parameters = get_signature(method).parameters
if "self" not in parameters:
Expand Down
11 changes: 9 additions & 2 deletions docs/core/binding.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Binding

In order to derive a service implementation, Combadge inspects a provided protocol and extract its methods and the method's signatures. The latter are used to derive request and response models.
In order to derive a service implementation, Combadge inspects the provided protocol and extract its methods and the method's signatures. The latter are used to derive request and response models.

Result of binding is a service class which encapsulates request and response handling.
Result of the binding is a service class which encapsulates request and response handling.

## Which methods are inspected?

- Non-private instance methods – methods which names do not start with `_` and accept `self` as a parameter.
- [`__call__`][1] which allows calling a bound client directly. This may be useful when the protocol is meant to represent a single method and would otherwise just result in the name duplication.

[1]: https://docs.python.org/3/reference/datamodel.html#object.__call__
9 changes: 8 additions & 1 deletion tests/core/test_binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ class TestService(SupportsService, Protocol):
def invoke(self) -> None:
raise NotImplementedError

assert list(_enumerate_methods(TestService)) == [("invoke", TestService.invoke)]
@abstractmethod
def __call__(self) -> None:
raise NotImplementedError

assert list(_enumerate_methods(TestService)) == [
("__call__", TestService.__call__),
("invoke", TestService.invoke),
]


def test_enumerate_class_methods() -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
connection:
- keep-alive
host:
- httpbin.org
user-agent:
- python-httpx/0.27.2
method: GET
uri: https://httpbin.org/user-agent
response:
body:
string: "{\n \"user-agent\": \"python-httpx/0.27.2\"\n}\n"
headers:
Access-Control-Allow-Credentials:
- 'true'
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '42'
Content-Type:
- application/json
Date:
- Wed, 13 Nov 2024 15:33:20 GMT
Server:
- gunicorn/19.9.0
status:
code: 200
message: OK
version: 1
28 changes: 24 additions & 4 deletions tests/integration/test_httpbin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest
from httpx import AsyncClient, Client
from pydantic import BaseModel
from pydantic import BaseModel, Field

from combadge.core.errors import BackendError
from combadge.core.interfaces import SupportsService
Expand Down Expand Up @@ -99,9 +99,11 @@ def get_headers(

@pytest.mark.vcr
async def test_headers_async() -> None:
class Response(BaseModel):
headers: dict[str, Any]
content_length: int
"""
Test custom headers in an asynchronous call.
# TODO: I suspect that the separate test for `async` is not needed as the code does not care.
"""

class SupportsHttpbin(SupportsService, Protocol):
@http_method("GET")
Expand Down Expand Up @@ -150,3 +152,21 @@ def get_internal_server_error(self) -> None: ...
service = SyncHttpxBackend(Client(base_url="https://httpbin.org"))[SupportsHttpbin] # type: ignore[type-abstract]
with pytest.raises(BackendError):
service.get_internal_server_error()


@pytest.mark.vcr
def test_callback_protocol() -> None:
"""Test that the `__call__()` method can actually be used."""

class Response(BaseModel):
user_agent: Annotated[str, Field(validation_alias="user-agent")]

class SupportsHttpbin(Protocol):
@http_method("GET")
@path("/user-agent")
@abstractmethod
def __call__(self) -> Response:
raise NotImplementedError

service = SyncHttpxBackend(Client(base_url="https://httpbin.org"))[SupportsHttpbin] # type: ignore[type-abstract]
assert service().user_agent == "python-httpx/0.27.2"

0 comments on commit 3e3bd8b

Please sign in to comment.