From 9f6a16fb759afb9d20ba22a8b6b50b6824d533d3 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 4 Jun 2026 12:51:02 +0300 Subject: [PATCH 1/3] docs: spec for FastStream 0.7.1 TestBroker typing alignment Co-Authored-By: Claude Opus 4.7 (1M context) --- ...ststream-0.7.1-testbroker-typing-design.md | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 planning/specs/2026-06-04-faststream-0.7.1-testbroker-typing-design.md diff --git a/planning/specs/2026-06-04-faststream-0.7.1-testbroker-typing-design.md b/planning/specs/2026-06-04-faststream-0.7.1-testbroker-typing-design.md new file mode 100644 index 0000000..53a9889 --- /dev/null +++ b/planning/specs/2026-06-04-faststream-0.7.1-testbroker-typing-design.md @@ -0,0 +1,209 @@ +# FastStream 0.7.1 TestBroker typing alignment — design + +**Status:** Draft +**Date:** 2026-06-04 +**Slug:** `faststream-0.7.1-testbroker-typing` + +## Goal + +Adopt the upstream `TestBroker` typing fix shipped in FastStream 0.7.1 +(ag2ai/faststream#2903) by binding the new `EnterType` generic to +`OutboxBroker` and removing the two `# ty: ignore` directives that +worked around the same upstream bug in our codebase. + +## Background + +In FastStream 0.7.0, `TestBroker.__aenter__` was annotated +`Broker | list[Broker]`. That union made the natural usage shape fail +the type checker: + +```python +async with TestOutboxBroker(OutboxBroker(...)) as br: + await br.publish(...) + # error: Item "list[OutboxBroker]" of "OutboxBroker | list[OutboxBroker]" + # has no attribute "publish" +``` + +The sibling project `faststream-redis-timers` worked around this by +overriding `__aenter__` to return a single broker directly (plus an +`isinstance(list)` assert). **This project took a different route**: +it suppressed the resulting `ty` diagnostics with two targeted ignores +and never wrote an `__aenter__` override. + +FastStream 0.7.1 (PR ag2ai/faststream#2903) fixes the root cause: + +1. `TestBroker` becomes `Generic[Broker, EnterType]`. `EnterType` uses + `typing_extensions.TypeVar` with `default=Any` for backward compatibility. +2. `__aenter__` returns `EnterType` instead of `Broker | list[Broker]`. +3. Each concrete subclass adds two `@overload`s on `__init__` that bind + `EnterType` to either `SomeBroker` (single) or `tuple[SomeBroker, ...]` + (multi). Note the multi case now returns a `tuple`, not a `list`. +4. The ASGI registry annotation in `try_it_out.py` becomes + `TestBroker[Any, Any]`. +5. The AST-inspection helper in `_internal/testing/ast.py` learns to walk + past *any* number of `__init__` frames, so subclasses (like ours) that + add their own `__init__` continue to work. + +Because `EnterType` defaults to `Any`, our existing `TestBroker[OutboxBroker]` +annotation would still type-check under 0.7.1 — but the two `ty: ignore` +suppressions can come off once we bind `EnterType = OutboxBroker` and +align the registry-hook annotation to the new two-param shape. + +## Scope + +### In scope + +- Drop `# ty: ignore[invalid-type-arguments]` on the `TestOutboxBroker` + class declaration in `faststream_outbox/testing.py`, switching the + generic to `TestBroker[OutboxBroker, OutboxBroker]`. +- Drop `# ty: ignore[invalid-return-type]` on the `get_broker_registry` + return in `faststream_outbox/__init__.py`, updating the annotation to + `TestBroker[typing.Any, typing.Any]` to match upstream's new registry + signature. +- Bump the FastStream pin in `pyproject.toml`: + `faststream>=0.7,<0.8` → `faststream>=0.7.1,<0.8`. +- Add one regression test in `tests/test_fake.py` ensuring + `async with TestOutboxBroker(broker)` yields a single `OutboxBroker` + (not a list or tuple). + +### Out of scope + +- Multi-broker `TestOutboxBroker` support (current `__init__` accepts a + single broker; we have no use case for multi). +- The `# ty: ignore[invalid-argument-type]` on `patch_broker_calls(broker)` + in `testing.py:_fake_start` — this is unrelated (config-generic + invariance on `BrokerUsecase[Msg, Conn, BrokerConfig]`) and is + documented in `CLAUDE.md` under the publisher/producer ignore table. +- The `# ty: ignore[missing-argument]` directives on `broker.publish(...)` + calls inside `tests/test_fake.py` — also unrelated (test broker + patches `publish` to make `session` optional in tests; `ty` sees the + original signature). Documented in `CLAUDE.md`. +- Any behavioral or runtime change. +- Refactors elsewhere in the package. + +## Detailed changes + +### `faststream_outbox/testing.py` + +Current (line 521): + +```python +class TestOutboxBroker(TestBroker[OutboxBroker]): # ty: ignore[invalid-type-arguments] +``` + +After: + +```python +class TestOutboxBroker(TestBroker[OutboxBroker, OutboxBroker]): +``` + +Binding `EnterType = OutboxBroker` makes `__aenter__` return +`OutboxBroker` directly. The class no longer raises +`invalid-type-arguments` from `ty`, so the suppression comes off. + +### `faststream_outbox/__init__.py` + +Current (lines 45–47): + +```python +@functools.lru_cache(maxsize=1) +def get_broker_registry() -> dict[type[BrokerUsecase[typing.Any, typing.Any]], type[TestBroker[typing.Any]]]: + return {**original_get_broker_registry(), OutboxBroker: TestOutboxBroker} # ty: ignore[invalid-return-type] +``` + +After: + +```python +@functools.lru_cache(maxsize=1) +def get_broker_registry() -> dict[ + type[BrokerUsecase[typing.Any, typing.Any]], + type[TestBroker[typing.Any, typing.Any]], +]: + return {**original_get_broker_registry(), OutboxBroker: TestOutboxBroker} +``` + +Matches upstream's new `try_it_out._get_broker_registry` signature. +With both type params present and `TestOutboxBroker` now declared +`TestBroker[OutboxBroker, OutboxBroker]`, the return value is +structurally assignable and the ignore comes off. + +### `pyproject.toml` + +Current (line 12): `"faststream>=0.7,<0.8",` +After: `"faststream>=0.7.1,<0.8",` + +### `tests/test_fake.py` — new regression test + +Appended to the end of `tests/test_fake.py`: + +```python +async def test_test_broker_aenter_returns_single_outbox_broker() -> None: + """0.7.1's EnterType binding means TestOutboxBroker yields a single OutboxBroker, not a list/tuple. + + Guards the contract through the upstream typing refactor: even if the base + class signature changes again, our single-broker subclass must always hand + back a single broker instance. + """ + broker = _make_broker() + async with TestOutboxBroker(broker) as br: + assert isinstance(br, OutboxBroker) +``` + +The single `isinstance(br, OutboxBroker)` assertion is sufficient: +since `OutboxBroker` is not a `list` or `tuple` subclass, an extra +`assert not isinstance(br, (list, tuple))` adds no additional safety. +The docstring covers the intent. + +No new imports needed — `TestOutboxBroker`, `OutboxBroker`, and the +`_make_broker()` helper are already in scope at the top of +`tests/test_fake.py`. The test follows the existing `_make_broker()` +pattern used throughout the file, not the integration-style +`OutboxBroker(engine=...)` construction. + +## Validation + +Run in order: + +1. `just install` — pull in `faststream==0.7.1`. +2. `just lint` — confirm `ruff`, `ruff format`, and `ty check` are all + clean after removing the two ignores. **If `ty` flags a *different* + issue on either annotation, stop and investigate** rather than + re-adding the original suppression — that would mean upstream's + 0.7.1 fix isn't behaving as documented in our environment. +3. `just test` — full suite under docker compose. Of particular interest: + - `tests/test_fake.py` — every test uses `async with TestOutboxBroker(broker)`. + If `EnterType` were wired wrong, `br.publish(...)` and similar calls + would explode at type-check time and possibly at runtime under + stricter Python. + - The new regression test from `tests/test_fake.py`. + +## Risks + +- **Upstream AST helper walking past our `__init__` frame.** PR #2903 + explicitly handles arbitrary `__init__` depth via a `while … name == + "__init__"` walk in `_internal/testing/ast.py`. `TestOutboxBroker` + adds an extra `__init__` frame on top of `TestBroker.__init__`; the + full `test_fake.py` suite exercises this path. No action needed. +- **`uv lock --upgrade` pulling in unrelated upgrades.** `just install` + refreshes all dependencies. If incidental breakage surfaces, narrow + the upgrade to `uv lock --upgrade-package faststream` and re-run + `uv sync --frozen` to avoid pulling in unrelated changes. +- **Pinning out 0.7.0 consumers.** The previous release already + required `>=0.7`; bumping to `>=0.7.1` is a trivial floor increment. + No migration note needed. +- **`ty` still flags a different diagnostic after the changes.** If + this happens, document the new suppression with a tight justification + (matching the format in `CLAUDE.md`'s ignore table) rather than + re-adding the originals — the originals targeted bugs that 0.7.1 + fixes upstream, so they would be misleading. + +## Rollout + +- Single PR on branch `chore/faststream-0.7.1-testbroker-typing`, + matching the sibling project's naming convention. +- Bundled commit (the pin bump and the suppression removals are tightly + coupled — the removals are only safe once we require 0.7.1+). +- Follows the project workflow in `CLAUDE.md`: + brainstorming → spec → writing-plans → plan → + executing-plans / subagent-driven-development → + requesting-code-review → finishing-a-development-branch. From 7659bd06333d698578da9d1319d95746e685f11f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 4 Jun 2026 12:53:22 +0300 Subject: [PATCH 2/3] docs: plan for FastStream 0.7.1 TestBroker typing alignment Co-Authored-By: Claude Opus 4.7 (1M context) --- ...faststream-0.7.1-testbroker-typing-plan.md | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 planning/plans/2026-06-04-faststream-0.7.1-testbroker-typing-plan.md diff --git a/planning/plans/2026-06-04-faststream-0.7.1-testbroker-typing-plan.md b/planning/plans/2026-06-04-faststream-0.7.1-testbroker-typing-plan.md new file mode 100644 index 0000000..a4622a0 --- /dev/null +++ b/planning/plans/2026-06-04-faststream-0.7.1-testbroker-typing-plan.md @@ -0,0 +1,256 @@ +# FastStream 0.7.1 TestBroker typing alignment — implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Adopt FastStream 0.7.1's `TestBroker[Broker, EnterType]` typing fix here by binding `EnterType = OutboxBroker` and removing the two `# ty: ignore` directives that worked around the upstream `Broker | list[Broker]` return-type bug. + +**Architecture:** Bump the FastStream pin to `>=0.7.1`, switch `TestOutboxBroker`'s generic to `TestBroker[OutboxBroker, OutboxBroker]`, update the ASGI registry hook return annotation to the new two-param shape, delete the two suppressions that the upstream fix obsoletes, and add a regression test in `tests/test_fake.py` that proves the entered context yields a single `OutboxBroker` (not a list/tuple). + +**Tech Stack:** Python 3.13, FastStream 0.7.1, `uv` for deps, `ruff` for lint, `ty` for type check, `pytest` (under docker compose for the Postgres-backed suite). + +**Spec:** [`planning/specs/2026-06-04-faststream-0.7.1-testbroker-typing-design.md`](../specs/2026-06-04-faststream-0.7.1-testbroker-typing-design.md) + +**Commit strategy:** Single bundled commit at the end of Task 5. Tasks 1–4 stage incrementally without committing, so intermediate `ty check` runs reflect work-in-progress and the final commit captures one logical change. + +**Branch:** `chore/faststream-0.7.1-testbroker-typing` (matches the sibling project's naming). + +--- + +### Task 1: Bump the FastStream pin to >=0.7.1 + +**Files:** +- Modify: `pyproject.toml:12` + +- [ ] **Step 1: Create the feature branch from main** + +Run: `git switch -c chore/faststream-0.7.1-testbroker-typing` +Expected: switched to a new branch off `main`. + +- [ ] **Step 2: Edit the dependency line** + +In `pyproject.toml`, change: + +```toml +"faststream>=0.7,<0.8", +``` + +to: + +```toml +"faststream>=0.7.1,<0.8", +``` + +- [ ] **Step 3: Refresh the lockfile and sync** + +Run: `just install` + +This runs `uv lock --upgrade` followed by `uv sync --all-extras --all-groups --frozen`. Expected: `uv` resolves `faststream==0.7.1` (the only version satisfying both `>=0.7.1` and `<0.8` at time of writing). The sync should report no errors. + +- [ ] **Step 4: Confirm the resolved version** + +Run: `uv pip show faststream | grep -i version` +Expected output: `Version: 0.7.1` + +- [ ] **Step 5: Sanity-run the no-Postgres tests against the upgraded library** + +Run: `uv run pytest tests/test_unit.py tests/test_fake.py -v --no-cov` +Expected: all tests pass. (The two `ty: ignore` directives are still in place and still satisfy the type checker under 0.7.1, because `EnterType` defaults to `Any`. If anything fails here, **stop** — the upgrade has surfaced an unrelated regression that needs investigation before refactoring.) `--no-cov` is required because partial runs would otherwise trip the `--cov-fail-under=100` ratchet from `pyproject.toml`'s `addopts`. + +- [ ] **Step 6: Stage the change (do not commit yet)** + +Run: `git add pyproject.toml uv.lock` + +(Verify `uv.lock` is tracked first with `git status`; if it isn't, drop it from the `git add`.) + +--- + +### Task 2: Add the regression test for `__aenter__` return shape + +**Files:** +- Modify: `tests/test_fake.py` (append new test at the end of the file) + +This test is added *before* the refactor so we can prove the contract (single `OutboxBroker` returned from the context, never a list or tuple) survives the suppression removal. Under the current code (where `TestBroker[OutboxBroker]`'s `__aenter__` returns `OutboxBroker | list[OutboxBroker]` and we suppress with `ty: ignore`) the test should pass on the first run because the runtime `__aenter__` already returns the single broker — the bug was purely in the type annotation. + +- [ ] **Step 1: Append the new test to `tests/test_fake.py`** + +Add this test as the last function in the file (after `test_fake_dlq_not_emitted_on_handler_success`): + +```python +async def test_test_broker_aenter_returns_single_outbox_broker() -> None: + """0.7.1's EnterType binding means TestOutboxBroker yields a single OutboxBroker, not a list/tuple. + + Guards the contract through the upstream typing refactor: even if the base + class signature changes again, our single-broker subclass must always hand + back a single broker instance. + """ + broker = _make_broker() + async with TestOutboxBroker(broker) as br: + assert isinstance(br, OutboxBroker) +``` + +No new imports needed — `TestOutboxBroker`, `OutboxBroker`, and the module-local `_make_broker()` helper are already in scope in `tests/test_fake.py`. + +- [ ] **Step 2: Run the new test to confirm it passes against the current code** + +Run: `uv run pytest tests/test_fake.py::test_test_broker_aenter_returns_single_outbox_broker -v --no-cov` +Expected: PASS. (The runtime `__aenter__` already returns the single `OutboxBroker`; we're locking that contract in.) + +- [ ] **Step 3: Stage the change** + +Run: `git add tests/test_fake.py` + +--- + +### Task 3: Refactor `TestOutboxBroker` — bind `EnterType`, drop the type-arguments suppression + +**Files:** +- Modify: `faststream_outbox/testing.py:521` + +- [ ] **Step 1: Replace the class declaration** + +In `faststream_outbox/testing.py`, locate line 521: + +```python +class TestOutboxBroker(TestBroker[OutboxBroker]): # ty: ignore[invalid-type-arguments] +``` + +Replace with: + +```python +class TestOutboxBroker(TestBroker[OutboxBroker, OutboxBroker]): +``` + +The base `__aenter__` now returns `EnterType`, which we bind to `OutboxBroker`. The `invalid-type-arguments` suppression that worked around the 0.7.0 single-param shape is no longer needed. + +**Do not touch** the `# ty: ignore[invalid-argument-type]` on `patch_broker_calls(broker)` later in the same file (around line 626) — that's unrelated (config-generic invariance on `BrokerUsecase`) and is documented in `CLAUDE.md`'s ignore table. + +- [ ] **Step 2: Confirm `ty` is satisfied with the new annotation** + +Run: `uv run ty check faststream_outbox/testing.py` +Expected: no errors. (If `ty` flags a *different* diagnostic on the class declaration, **stop** — the upstream 0.7.1 fix isn't behaving as documented in our environment; investigate before adding a replacement suppression.) + +- [ ] **Step 3: Re-run the regression test from Task 2** + +Run: `uv run pytest tests/test_fake.py::test_test_broker_aenter_returns_single_outbox_broker -v --no-cov` +Expected: PASS. (The result now comes from the base class via `EnterType = OutboxBroker` instead of falling through the union.) + +- [ ] **Step 4: Re-run the full `test_fake.py` suite to confirm no regression** + +Run: `uv run pytest tests/test_fake.py -v --no-cov` +Expected: every test passes. Every test in this file uses `async with TestOutboxBroker(broker)`; if `EnterType` were wired wrong, the subsequent `await broker.publish(...)` calls would still work at runtime (the override never affected runtime), but any `ty` mismatch would surface here when the next steps run `just lint`. + +- [ ] **Step 5: Stage the change** + +Run: `git add faststream_outbox/testing.py` + +--- + +### Task 4: Update the ASGI registry hook annotation — drop the return-type suppression + +**Files:** +- Modify: `faststream_outbox/__init__.py:45-47` + +- [ ] **Step 1: Update the return type annotation and drop the inline ignore** + +In `faststream_outbox/__init__.py`, locate lines 45–47: + +```python + @functools.lru_cache(maxsize=1) + def get_broker_registry() -> dict[type[BrokerUsecase[typing.Any, typing.Any]], type[TestBroker[typing.Any]]]: + return {**original_get_broker_registry(), OutboxBroker: TestOutboxBroker} # ty: ignore[invalid-return-type] +``` + +Replace with: + +```python + @functools.lru_cache(maxsize=1) + def get_broker_registry() -> dict[ + type[BrokerUsecase[typing.Any, typing.Any]], + type[TestBroker[typing.Any, typing.Any]], + ]: + return {**original_get_broker_registry(), OutboxBroker: TestOutboxBroker} +``` + +This matches the new shape of `faststream.asgi.factories.asyncapi.try_it_out._get_broker_registry`, which 0.7.1 typed as `dict[..., type[TestBroker[Any, Any]]]`. With `TestOutboxBroker` now declared `TestBroker[OutboxBroker, OutboxBroker]` (from Task 3), the dict value type is structurally assignable and the `invalid-return-type` suppression comes off cleanly. + +- [ ] **Step 2: Confirm `ty` is satisfied with the new annotation** + +Run: `uv run ty check faststream_outbox/__init__.py` +Expected: no errors. (Same stop-and-investigate rule as Task 3 Step 2 if `ty` flags a different diagnostic.) + +- [ ] **Step 3: Stage the change** + +Run: `git add faststream_outbox/__init__.py` + +--- + +### Task 5: Final validation and bundled commit + +**Files:** +- All four changes above are now staged together. + +- [ ] **Step 1: Lint the staged changes** + +Run: `just lint` +Expected: `eof-fixer`, `ruff format`, `ruff check --fix`, and `ty check` all pass. If `ruff format` or `ruff check --fix` modifies any staged file, re-stage with `git add ` and re-run `just lint` until clean. + +- [ ] **Step 2: Run the full test suite under docker compose** + +Run: `just test` +Expected: every test in `tests/test_unit.py`, `tests/test_fake.py`, and `tests/test_integration.py` passes, and the `--cov-fail-under=100` ratchet is satisfied. The new regression test from Task 2 should appear in the output and pass. + +- [ ] **Step 3: Review staged diff one more time** + +Run: `git diff --staged` +Expected: changes only in `pyproject.toml`, `uv.lock` (if tracked), `tests/test_fake.py`, `faststream_outbox/testing.py`, and `faststream_outbox/__init__.py`. No drive-by edits. + +- [ ] **Step 4: Commit** + +Run: + +```bash +git commit -m "$(cat <<'EOF' +chore: adopt faststream 0.7.1 TestBroker typing fix + +ag2ai/faststream#2903 makes TestBroker generic over a second EnterType +TypeVar (default Any) and threads it through __aenter__. Bind +EnterType = OutboxBroker in our TestOutboxBroker and drop the +# ty: ignore[invalid-type-arguments] on the class declaration plus the +# ty: ignore[invalid-return-type] on get_broker_registry's return that +worked around the old Broker | list[Broker] return type. Update the +ASGI registry annotation to the new two-param shape and bump the +faststream floor to >=0.7.1. + +Adds a regression test guarding the single-broker contract through +future upstream changes. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" + +``` + +- [ ] **Step 5: Verify clean tree** + +Run: `git status` +Expected: working tree clean; one new commit on `chore/faststream-0.7.1-testbroker-typing`. + +--- + +## Validation summary (post-implementation) + +After Task 5, the following should hold: + +- `pyproject.toml` requires `faststream>=0.7.1,<0.8`. +- `TestOutboxBroker` declares `TestBroker[OutboxBroker, OutboxBroker]` with **no** `# ty: ignore` on the class declaration. +- `get_broker_registry` returns `dict[..., type[TestBroker[typing.Any, typing.Any]]]` with **no** `# ty: ignore` on the return statement. +- The `# ty: ignore[invalid-argument-type]` on `patch_broker_calls(broker)` in `_fake_start` is **untouched** (documented in `CLAUDE.md` as a separate concern). +- `tests/test_fake.py::test_test_broker_aenter_returns_single_outbox_broker` is present and passing. +- `just lint` and `just test` both succeed. + +## Risks & mitigations + +- **Upstream AST helper depth.** `TestOutboxBroker.__init__` adds an extra frame on top of `TestBroker.__init__`. The 0.7.1 PR adds a `while … name == "__init__"` walk in `_internal/testing/ast.py` exactly for this, so the `async with` AST analysis still finds the user frame. Validated implicitly by Task 3 Step 4 (the full `test_fake.py` suite, which depends on this mechanism). No code change required on our side. +- **`uv lock --upgrade` pulling in unrelated upgrades.** `just install` runs `uv lock --upgrade` which refreshes *all* dependencies. If this surfaces incidental breakage in Task 1 Step 3, narrow the upgrade to `uv lock --upgrade-package faststream` and re-run `uv sync --all-extras --all-groups --frozen` to avoid pulling in unrelated changes. +- **`just test` requires Docker.** If Docker isn't running locally, `just test` will fail at the compose step. Start Docker before Task 5 Step 2. The no-Docker subset (`tests/test_unit.py`, `tests/test_fake.py`) already ran in Task 1 Step 5 and Task 3 Step 4; Task 5 Step 2 is what actually exercises `tests/test_integration.py` against real Postgres and satisfies the 100% coverage ratchet. +- **`ty` still flagging diagnostics after the suppressions come off.** If Task 3 Step 2 or Task 4 Step 2 surfaces a *different* `ty` diagnostic, the playbook is to investigate (not re-add the original ignore). The original suppressions targeted bugs that 0.7.1 fixes; a new diagnostic would mean either the upstream fix isn't behaving as documented in our environment, or our annotation has a separate issue that deserves its own justification entry in `CLAUDE.md`'s ignore table. From 7842e8d10fd472f37a897dcbaa59a136dd85253f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Thu, 4 Jun 2026 13:15:03 +0300 Subject: [PATCH 3/3] chore: adopt faststream 0.7.1 TestBroker typing fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ag2ai/faststream#2903 makes TestBroker generic over a second EnterType TypeVar (default Any) and threads it through __aenter__. Bind EnterType = OutboxBroker in our TestOutboxBroker so the entered context is typed as a single OutboxBroker rather than OutboxBroker | list[OutboxBroker]. Update the ASGI registry hook annotation to the new two-param shape and bump the faststream floor to >=0.7.1. Both pre-existing ty suppressions stay: git blame shows the class-line # ty: ignore[invalid-type-arguments] on TestOutboxBroker predates 0.7.1 and masks a separate issue (BrokerUsecase invariance on its config-type param, the same root cause already documented for patch_broker_calls). The # ty: ignore[invalid-return-type] on get_broker_registry's dict literal is the same generic-invariance story applied to the dict key side — empirically still flagged on 0.7.1 even with the two-param TestBroker annotation. The spec's claim that 0.7.1 would obsolete either suppression was incorrect; ignore-table justifications in CLAUDE.md updated accordingly. Adds a regression test guarding the single-broker contract through future upstream changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 ++ faststream_outbox/__init__.py | 5 ++++- faststream_outbox/testing.py | 2 +- pyproject.toml | 2 +- tests/test_fake.py | 13 +++++++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0990a6d..ae5c105 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -193,6 +193,8 @@ Audit of the suppressions added by the publisher feature. Each is intentional; b | `# ty: ignore[invalid-argument-type]` on `super().__init__(config, specification)` | `publisher/usecase.py:OutboxPublisher.__init__` | Passing `OutboxPublisherSpecification` where the base `PublisherUsecase` expects `PublisherSpecification[BrokerConfig, ...]`. Generic invariance — identical pattern in `faststream-sqlbroker` and `faststream-redis-timers`. | | `# ty: ignore[invalid-method-override]` on `.publish` | `publisher/usecase.py:OutboxPublisher.publish`, `broker.py:OutboxBroker.publish` / `publish_batch` | Overriding `PublisherProto.publish(message, /, *, correlation_id)` with outbox-specific kwargs (`session`, `activate_in`, `activate_at`, `timer_id`). The outbox contract diverges from the FastStream default — that's the whole point. | | `# ty: ignore[invalid-argument-type]` on `patch_broker_calls(broker)` | `testing.py:_fake_start` | `BrokerUsecase[Msg, Conn, BrokerConfig]` is invariant on the config type; `OutboxBroker`'s `OutboxBrokerConfig` triggers the diagnostic. The call only iterates `broker.subscribers` — runtime-safe. | +| `# ty: ignore[invalid-type-arguments]` on `class TestOutboxBroker(TestBroker[OutboxBroker, OutboxBroker])` | `testing.py:TestOutboxBroker` | Same root cause as the `patch_broker_calls` row: `BrokerUsecase` is invariant on its config-type param, and `OutboxBroker` extends `BrokerUsecase[..., ..., OutboxBrokerConfig]` (an `OutboxBrokerConfig`, not `BrokerConfig`), so `ty` rejects it against the `Broker` TypeVar's bound `BrokerUsecase[Any, Any, BrokerConfig]`. Predates the FastStream 0.7.1 `EnterType` TypeVar split and survives it — not a workaround for the 0.7.0 union-return-type bug. | +| `# ty: ignore[invalid-return-type]` on the ASGI registry dict literal | `__init__.py:get_broker_registry` | Same root cause again: the dict-comprehension's `OutboxBroker` key is `type[OutboxBroker]`, which `ty` won't unify with the annotated `type[BrokerUsecase[Any, Any]]` because of `BrokerUsecase`'s config-type invariance. The two-param `TestBroker[Any, Any]` form (0.7.1) is otherwise clean — only the key-side widening trips the diagnostic. | | `# noqa: SLF001` on private-attr access | `testing.py` (`fd_config._serializer`, `sub._config.*`, `sub._worker_loop`, `sub._fetch_loop`), `broker.py:fd_config._serializer`, `__init__.py:_get_broker_registry` | Test broker and broker init reach into FastStream internals to thread serializers and start the real subscriber loops against the fake client. No public surface for these. | | `# ty: ignore[missing-argument]` / `[invalid-argument-type]` on `broker.publish(...)` in tests | `tests/test_fake.py` | `TestOutboxBroker` patches `broker.publish` to make `session` optional in tests. `ty` sees the original signature. | | `# pragma: no cover` on `TestOutboxBroker.create_publisher_fake_subscriber` | `testing.py` | `_fake_start` deliberately bypasses FastStream's publisher fake-subscriber loop (would mock the real handler). The abstract method must exist but is genuinely unreachable in normal flows. | diff --git a/faststream_outbox/__init__.py b/faststream_outbox/__init__.py index 93c16d4..59468d6 100644 --- a/faststream_outbox/__init__.py +++ b/faststream_outbox/__init__.py @@ -43,7 +43,10 @@ original_get_broker_registry = faststream.asgi.factories.asyncapi.try_it_out._get_broker_registry # noqa: SLF001 @functools.lru_cache(maxsize=1) - def get_broker_registry() -> dict[type[BrokerUsecase[typing.Any, typing.Any]], type[TestBroker[typing.Any]]]: + def get_broker_registry() -> dict[ + type[BrokerUsecase[typing.Any, typing.Any]], + type[TestBroker[typing.Any, typing.Any]], + ]: return {**original_get_broker_registry(), OutboxBroker: TestOutboxBroker} # ty: ignore[invalid-return-type] faststream.asgi.factories.asyncapi.try_it_out._get_broker_registry = get_broker_registry # noqa: SLF001 diff --git a/faststream_outbox/testing.py b/faststream_outbox/testing.py index f80799c..3035511 100644 --- a/faststream_outbox/testing.py +++ b/faststream_outbox/testing.py @@ -518,7 +518,7 @@ async def fake_fetch_unprocessed( return fake_fetch_unprocessed -class TestOutboxBroker(TestBroker[OutboxBroker]): # ty: ignore[invalid-type-arguments] +class TestOutboxBroker(TestBroker[OutboxBroker, OutboxBroker]): # ty: ignore[invalid-type-arguments] """ Test harness for ``OutboxBroker``. Two dispatch modes. diff --git a/pyproject.toml b/pyproject.toml index d5917bb..6a4244f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" requires-python = ">=3.13,<4" license = "MIT" dependencies = [ - "faststream>=0.7,<0.8", + "faststream>=0.7.1,<0.8", "sqlalchemy[asyncio]>=2.0", ] diff --git a/tests/test_fake.py b/tests/test_fake.py index bbf42e8..e8c58ff 100644 --- a/tests/test_fake.py +++ b/tests/test_fake.py @@ -1442,3 +1442,16 @@ def recorder(event: str, tags: typing.Any) -> None: assert test_broker.fake_client.dlq_rows == [] # And the row should be deleted (handler succeeded). assert test_broker.fake_client.rows == [] + + +async def test_test_broker_aenter_returns_single_outbox_broker() -> None: + """ + 0.7.1's EnterType binding means TestOutboxBroker yields a single OutboxBroker, not a list/tuple. + + Guards the contract through the upstream typing refactor: even if the base + class signature changes again, our single-broker subclass must always hand + back a single broker instance. + """ + broker = _make_broker() + async with TestOutboxBroker(broker) as br: + assert isinstance(br, OutboxBroker)