Skip to content

Commit e879eb8

Browse files
lesnik512claude
andcommitted
fix(chain): hoist Middleware/AsyncMiddleware imports out of TYPE_CHECKING
compose_async and compose used string annotations referencing AsyncMiddleware / Middleware imported only under `if typing.TYPE_CHECKING:`. Calling typing.get_type_hints(compose_async) at runtime raised `NameError: name 'AsyncMiddleware' is not defined`. The TYPE_CHECKING guard also violates the project's typing-import-style memory rule. Drop the guard, import the protocols unconditionally at module top, and unquote the string annotations. httpware.middleware.__init__ does not import chain.py back, so no circular import. Tests pin typing.get_type_hints(compose_async) and typing.get_type_hints( compose) resolve without NameError. Closes audit Low finding (chain.py:9-10) from planning/audit/2026-06-07-deep-audit.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f1ba1a8 commit e879eb8

2 files changed

Lines changed: 23 additions & 8 deletions

File tree

‎src/httpware/middleware/chain.py‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55

66
import httpx2
77

8-
9-
if typing.TYPE_CHECKING:
10-
from httpware.middleware import AsyncMiddleware, Middleware
8+
from httpware.middleware import AsyncMiddleware, Middleware
119

1210

1311
_AsyncNext: typing.TypeAlias = Callable[[httpx2.Request], Awaitable[httpx2.Response]]
1412
_Next: typing.TypeAlias = Callable[[httpx2.Request], httpx2.Response]
1513

1614

17-
def compose_async(middleware: "Sequence[AsyncMiddleware]", terminal: _AsyncNext) -> _AsyncNext:
15+
def compose_async(middleware: Sequence[AsyncMiddleware], terminal: _AsyncNext) -> _AsyncNext:
1816
"""Fold `middleware` into a single callable around `terminal`.
1917
2018
The first middleware in the sequence is the outermost wrapper.
@@ -25,14 +23,14 @@ def compose_async(middleware: "Sequence[AsyncMiddleware]", terminal: _AsyncNext)
2523
return dispatch
2624

2725

28-
def _wrap(layer: "AsyncMiddleware", inner: _AsyncNext) -> _AsyncNext:
26+
def _wrap(layer: AsyncMiddleware, inner: _AsyncNext) -> _AsyncNext:
2927
async def call(request: httpx2.Request) -> httpx2.Response:
3028
return await layer(request, inner)
3129

3230
return call
3331

3432

35-
def compose(middleware: "Sequence[Middleware]", terminal: _Next) -> _Next:
33+
def compose(middleware: Sequence[Middleware], terminal: _Next) -> _Next:
3634
"""Fold sync `middleware` into a single callable around sync `terminal`.
3735
3836
The first middleware in the sequence is the outermost wrapper.
@@ -43,7 +41,7 @@ def compose(middleware: "Sequence[Middleware]", terminal: _Next) -> _Next:
4341
return dispatch
4442

4543

46-
def _wrap_sync(layer: "Middleware", inner: _Next) -> _Next:
44+
def _wrap_sync(layer: Middleware, inner: _Next) -> _Next:
4745
def call(request: httpx2.Request) -> httpx2.Response:
4846
return layer(request, inner)
4947

‎tests/test_middleware.py‎

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

33
import asyncio
4+
import typing
45
from http import HTTPStatus
56

67
import httpx2
@@ -13,7 +14,7 @@
1314
async_before_request,
1415
async_on_error,
1516
)
16-
from httpware.middleware.chain import compose_async
17+
from httpware.middleware.chain import compose, compose_async
1718

1819

1920
def _make_request(url: str = "https://example.test/x") -> httpx2.Request:
@@ -174,3 +175,19 @@ async def terminal(request: httpx2.Request) -> httpx2.Response: # noqa: ARG001
174175
dispatch = compose_async((swallow_all,), terminal)
175176
with pytest.raises(asyncio.CancelledError):
176177
await dispatch(_make_request())
178+
179+
180+
def test_compose_async_get_type_hints_resolves_without_nameerror() -> None:
181+
"""typing.get_type_hints(compose_async) must resolve to real classes, not raise NameError.
182+
183+
Pre-0.8.5: AsyncMiddleware was imported only under `if typing.TYPE_CHECKING`,
184+
so get_type_hints raised NameError at runtime.
185+
"""
186+
hints = typing.get_type_hints(compose_async)
187+
assert "middleware" in hints
188+
189+
190+
def test_compose_get_type_hints_resolves_without_nameerror() -> None:
191+
"""Sync mirror — sync `compose` get_type_hints must also resolve."""
192+
hints = typing.get_type_hints(compose)
193+
assert "middleware" in hints

0 commit comments

Comments
 (0)