Skip to content

fix: type the middleware factory to FastStream's construction contract - #39

Merged
lesnik512 merged 1 commit into
mainfrom
fix/ty-paramspec-middleware
Aug 10, 2026
Merged

fix: type the middleware factory to FastStream's construction contract#39
lesnik512 merged 1 commit into
mainfrom
fix/ty-paramspec-middleware

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Closes #38.

ty 0.0.69 added unbound-type-variable and the weekly dependency check went red:

error[unbound-type-variable]: ParamSpec `P` is not in scope
  --> modern_di_faststream/main.py:30:31

_DIMiddlewareFactory.__call__ declared *args: P.args, **kwargs: P.kwargs with P bound to nothing — never solvable, which is why the code carried two # ty: ignore[invalid-argument-type] comments conceding it did not match BrokerMiddleware. This declares FastStream's real contract instead, and both ignores come out.

Rationale, alternatives, and verification: planning/changes/2026-08-10.01-middleware-contract-typed.md.

Collapsing to functools.partial (the revisit trigger on the two-class-split decision) was built and measured, then rejected — partial types as (*args: Any, **kwargs: Any) and satisfies any protocol, so a contract break under it leaves ty silent. That file records the outcome and a new trigger.

Verified locally on the upgraded lock: just lint-ci clean, 6 passed, 100% coverage.

ty 0.0.69 added `unbound-type-variable` and the weekly dependency check
went red: `_DIMiddlewareFactory.__call__` declared `*args: P.args,
**kwargs: P.kwargs` with the ParamSpec `P` bound to nothing. It was never
solvable, which is why the code carried two
`# ty: ignore[invalid-argument-type]` comments conceding it did not match
`BrokerMiddleware`.

Declare FastStream's real contract instead --
`__call__(msg, /, *, context: ContextRepo) -> _DiMiddleware`, mirrored on
`_DiMiddleware.__init__` -- and drop `P` plus `typing.Generic[P]`. Both
ignore comments come out and `add_middleware(...)` now type-checks on its
own, so a future upstream contract change is a ty error at the call site
rather than a first-message runtime failure.

`msg: object` rather than `Any`: the protocol's message parameter is
contravariant and the value is only forwarded to `BaseMiddleware.__init__`.

This fired the revisit trigger on the two-class-split decision. Collapsing
to `functools.partial` was built and measured -- it is ~10 LOC shorter and
equally clean -- but rejected: `partial` types as `(*args: Any,
**kwargs: Any)` and satisfies any protocol, so breaking the construction
contract under it leaves ty silent. The decision file records the outcome
and a new trigger.

Closes #38

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lesnik512
lesnik512 merged commit 6380b01 into main Aug 10, 2026
6 checks passed
@lesnik512
lesnik512 deleted the fix/ty-paramspec-middleware branch August 10, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scheduled dependency check failed

1 participant