Skip to content

Commit 6474b57

Browse files
committed
fix: let phase decorator reprs handle callables without __qualname__
repr() of a middleware built by before_request, after_response, on_error or their async siblings no longer raises AttributeError for a functools.partial or a callable instance; it falls back to repr(f).
1 parent edfe82b commit 6474b57

3 files changed

Lines changed: 73 additions & 6 deletions

File tree

‎src/httpware/middleware/__init__.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def __call__(self, request: httpx2.Request, next: AsyncNext) -> httpx2.Res
4444
return await next(await f(request))
4545

4646
def __repr__(self) -> str:
47-
return f"<async_before_request({f.__qualname__})>" # ty: ignore[unresolved-attribute]
47+
return f"<async_before_request({getattr(f, '__qualname__', repr(f))})>"
4848

4949
return _BeforeRequestMiddleware()
5050

@@ -60,7 +60,7 @@ async def __call__(self, request: httpx2.Request, next: AsyncNext) -> httpx2.Res
6060
return await f(request, response)
6161

6262
def __repr__(self) -> str:
63-
return f"<async_after_response({f.__qualname__})>" # ty: ignore[unresolved-attribute]
63+
return f"<async_after_response({getattr(f, '__qualname__', repr(f))})>"
6464

6565
return _AfterResponseMiddleware()
6666

@@ -85,7 +85,7 @@ async def __call__(self, request: httpx2.Request, next: AsyncNext) -> httpx2.Res
8585
return result
8686

8787
def __repr__(self) -> str:
88-
return f"<async_on_error({f.__qualname__})>" # ty: ignore[unresolved-attribute]
88+
return f"<async_on_error({getattr(f, '__qualname__', repr(f))})>"
8989

9090
return _OnErrorMiddleware()
9191

@@ -110,7 +110,7 @@ def __call__(self, request: httpx2.Request, next: Next) -> httpx2.Response: # n
110110
return next(f(request))
111111

112112
def __repr__(self) -> str:
113-
return f"<before_request({f.__qualname__})>" # ty: ignore[unresolved-attribute]
113+
return f"<before_request({getattr(f, '__qualname__', repr(f))})>"
114114

115115
return _BeforeRequestMiddleware()
116116

@@ -126,7 +126,7 @@ def __call__(self, request: httpx2.Request, next: Next) -> httpx2.Response: # n
126126
return f(request, response)
127127

128128
def __repr__(self) -> str:
129-
return f"<after_response({f.__qualname__})>" # ty: ignore[unresolved-attribute]
129+
return f"<after_response({getattr(f, '__qualname__', repr(f))})>"
130130

131131
return _AfterResponseMiddleware()
132132

@@ -151,6 +151,6 @@ def __call__(self, request: httpx2.Request, next: Next) -> httpx2.Response: # n
151151
return result
152152

153153
def __repr__(self) -> str:
154-
return f"<on_error({f.__qualname__})>" # ty: ignore[unresolved-attribute]
154+
return f"<on_error({getattr(f, '__qualname__', repr(f))})>"
155155

156156
return _OnErrorMiddleware()

‎tests/test_middleware.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Tests for the AsyncMiddleware protocol, AsyncNext type, chain composition, and decorators."""
22

33
import asyncio
4+
import functools
45
import typing
6+
from collections.abc import Callable
57
from http import HTTPStatus
68

79
import httpx2
@@ -189,3 +191,34 @@ def test_compose_get_type_hints_resolves_without_nameerror() -> None:
189191
"""Sync mirror — sync `compose` get_type_hints must also resolve."""
190192
hints = typing.get_type_hints(compose)
191193
assert "middleware" in hints
194+
195+
196+
async def _passthrough(*args: typing.Any) -> typing.Any: # noqa: ANN401
197+
return args[-1] # pragma: no cover — never invoked; the test only checks repr()
198+
199+
200+
class _CallableTransform:
201+
async def __call__(self, *args: typing.Any) -> typing.Any: # noqa: ANN401
202+
return args[-1] # pragma: no cover — never invoked; the test only checks repr()
203+
204+
205+
@pytest.mark.parametrize(
206+
("decorator", "name"),
207+
[
208+
(async_before_request, "async_before_request"),
209+
(async_after_response, "async_after_response"),
210+
(async_on_error, "async_on_error"),
211+
],
212+
)
213+
@pytest.mark.parametrize(
214+
"factory",
215+
[lambda: functools.partial(_passthrough), _CallableTransform],
216+
ids=["partial", "callable_instance"],
217+
)
218+
def test_decorator_repr_falls_back_to_repr_without_qualname(
219+
decorator: Callable[[typing.Any], AsyncMiddleware],
220+
name: str,
221+
factory: Callable[[], typing.Any],
222+
) -> None:
223+
f = factory()
224+
assert repr(decorator(f)) == f"<{name}({f!r})>"

‎tests/test_middleware_sync.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Tests for the sync Middleware protocol, Next type, chain composition, and decorators."""
22

3+
import functools
4+
import typing
5+
from collections.abc import Callable
36
from http import HTTPStatus
47

58
import httpx2
@@ -197,3 +200,34 @@ def terminal_se(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001
197200
dispatch = compose((swallow_all,), terminal_se)
198201
with pytest.raises(SystemExit):
199202
dispatch(_make_request())
203+
204+
205+
def _passthrough(*args: typing.Any) -> typing.Any: # noqa: ANN401
206+
return args[-1] # pragma: no cover — never invoked; the test only checks repr()
207+
208+
209+
class _CallableTransform:
210+
def __call__(self, *args: typing.Any) -> typing.Any: # noqa: ANN401
211+
return args[-1] # pragma: no cover — never invoked; the test only checks repr()
212+
213+
214+
@pytest.mark.parametrize(
215+
("decorator", "name"),
216+
[
217+
(before_request, "before_request"),
218+
(after_response, "after_response"),
219+
(on_error, "on_error"),
220+
],
221+
)
222+
@pytest.mark.parametrize(
223+
"factory",
224+
[lambda: functools.partial(_passthrough), _CallableTransform],
225+
ids=["partial", "callable_instance"],
226+
)
227+
def test_decorator_repr_falls_back_to_repr_without_qualname(
228+
decorator: Callable[[typing.Any], Middleware],
229+
name: str,
230+
factory: Callable[[], typing.Any],
231+
) -> None:
232+
f = factory()
233+
assert repr(decorator(f)) == f"<{name}({f!r})>"

0 commit comments

Comments
 (0)