Repository navigation
Use anyio.from_thread.BlockingPortal in TestClient - #3498
Conversation
anyio 4.15 deprecated anyio.abc.BlockingPortal. The runtime type alias in testclient.py evaluated that name on import. Fixes #3497
Merging this PR will degrade performance by 7.44%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | test_multipart[file-512KiB] |
2.6 ms | 3.2 ms | -18.26% |
| ❌ | Simulation | test_multipart[boundary-like-file] |
8.1 ms | 9.4 ms | -13.28% |
| ⚡ | Simulation | test_urlencoded_form[field-1MiB-64KiB] |
9.4 ms | 8.4 ms | +11.86% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing vyrnsynx:fix-testclient-blockingportal-alias (d4e2950) with main (75735e1)
…tal deprecation (#732) ## Why the pytest matrix went red Every `pytest (3.11 / 3.12 / 3.13)` job on #731 fails at collection with ``` DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead. ``` raised from `starlette/testclient.py:53` (`_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]`) the moment a test module does `from fastapi.testclient import TestClient`. Seven server test modules (`test_batteries_serve`, `test_server`, `test_server_api_contract`, `test_server_code_intelligence`, `test_server_static_ui`, `test_server_usage`, `test_streaming_endpoints`) never collect, so the whole session is interrupted. **What moved, and when.** The workflow installs `pytest`, `fastapi`, `httpx` and `httpx2` unpinned via `uv run --with`. The last green run on `main` (2026-08-23) resolved anyio 4.14.2. anyio 4.15.0 shipped on 2026-09-02 (4.15.1 on 2026-09-05) and turned `anyio.abc.BlockingPortal` into a lazily-resolved deprecated alias that warns on access. starlette 1.6.0 (2026-08-08, still the latest release, pulled by fastapi 0.141.1) reads that alias at import time. With `filterwarnings = ["error"]` in `pyproject.toml`, the warning is an import-time exception. This is not a runner problem: #731 only changes `runs-on`, and the same failure reproduces locally with the workflow's exact `uv run` line against `main`. Nothing in this repo's own code touches anyio. ## Fix 1. **`pyproject.toml`**: one `filterwarnings` entry that ignores exactly that message, of exactly that category, from exactly the `starlette.testclient` module. This is a third-party warning with no released fix: starlette merged Kludex/starlette#3498 (switches to `anyio.from_thread.BlockingPortal`) but has not cut a release that contains it. Once a fixed starlette ships the entry is a no-op and can be dropped; the comment says so. Why not pin `anyio<4.15` instead: in `dependencies` it would constrain every `alfred-os` wheel user for a test-only warning; in the `dev` extra it would never be installed, because CI runs `uv run --with ...` without `--extra dev`; as a uv constraint it would not help a plain pip venv. The scoped ignore works for every install path and expires on its own. 2. **`tests/test_server.py`**: the collection failure was masking a second, unrelated drift. `test_api_memory_lessons_lists_active_lessons` hard-coded `valid_until=2026-08-30` and asserted `memory_status == "active"`, but the endpoint evaluates expiry against the wall clock, so the test turned red by itself on 2026-08-30 on all three Pythons. The expiry is now derived from `now + 90 days` and the serialised value is asserted against that same datetime. ## Verification - Reproduced with the workflow's exact `uv run --python X --with pytest --with fastapi --with httpx --with httpx2 pytest tests/` on 3.11, 3.12 and 3.13: identical 7 collection errors before the change. - `--with 'anyio<4.15'` alone also clears collection, confirming which package drifted. - After the change the seven server modules collect and pass on 3.11, 3.12 and 3.13. A full 3.13 run of the suite showed no other failure hiding behind the collection error (the only extra red was three release-gate tests that fail solely under a local git config with commit signing forced on, and pass with it neutralised). - `ruff check` / `ruff format --check` (0.15.22) clean; `bin/scrub-check.sh` clean. ## Follow-up for #731 #731 needs no change. Once this lands, rebase or re-run its CI (`gh run rerun <run-id> --failed`) and the pytest matrix should go green there too.
Backport the upstream BlockingPortal alias fix. AnyIO 4.15 deprecates anyio.abc.BlockingPortal, making starlette.testclient emit a warning that aborts python-openapi-core test collection with warnings as errors. Upstream: Kludex/starlette#3498
The lock file maintenance bump to anyio 4.15.1 deprecates the anyio.abc.BlockingPortal alias. starlette's testclient still imports it, which emits a DeprecationWarning that pyproject.toml's filterwarnings = "error" turns into a collection error, failing all test jobs. Unpin once Kludex/starlette#3498 is released. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BZFFKbxfBT1fUfzGpa8rXA
* Lock file maintenance * Pin anyio<4.15 to fix CI test failures The lock file maintenance bump to anyio 4.15.1 deprecates the anyio.abc.BlockingPortal alias. starlette's testclient still imports it, which emits a DeprecationWarning that pyproject.toml's filterwarnings = "error" turns into a collection error, failing all test jobs. Unpin once Kludex/starlette#3498 is released. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01BZFFKbxfBT1fUfzGpa8rXA --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Hiran Wijesinghe <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]>
`--superseded-by` was stored and never shown, so an unfiled run overtaken upstream read as plain `unsubmitted`. Two such runs are on disk: the starlette fix superseded by Kludex/starlette#3498 and the mypy#21964 fix superseded by python/mypy#21966, which the new competitor check found before filing. Co-Authored-By: Claude Opus 5 <[email protected]>
…ette ships #3498 anyio 4.15 (2026-09-02) deprecates the `anyio.abc.BlockingPortal` alias, which starlette's TestClient still imports at module load in its latest release (1.6.0, 2026-08-08). With `filterwarnings = ["error"]` the warning aborts conftest import: pytest exits with code 4 before running a single test, on every fresh install since the anyio release. Reproduced locally by pinning anyio==4.15.1. Dependencies are current; the fix simply is not published yet: starlette switched to `anyio.from_thread.BlockingPortal` on master on 2026-09-05 (Kludex/starlette#3498). The ignore is scoped to that one message and marked to be removed at the first starlette release after 1.6.0. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_0113DEa3m359srdD3TtkNm2n
* feat(ui): preset filter buttons via QUICK_FILTERS
Add a QUICK_FILTERS setting ("Label=needle;…") that renders preset buttons
under the search box. Clicking one fills the search with its needle;
clicking the active one clears it. The active preset is highlighted and
persisted with the other UI preferences.
A needle may hold several |-separated terms matched as OR, and the search
box now accepts the same syntax ("desktop|laptop"), so a single button can
cover a family of clients.
Parsing lives in Settings.quick_filter_list (labels default to the needle,
blanks and duplicate labels dropped, at most 20 entries, labels ≤ 40 chars,
validated at startup). Covered by tests/test_quick_filters.py (parsing,
rendering, HTML escaping) and a Playwright e2e test that exercises the
OR matching, the highlight and the toggle. The e2e `_serve` helper now
accepts Settings overrides for such fixtures.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0113DEa3m359srdD3TtkNm2n
* fix(ci): ignore the anyio 4.15 BlockingPortal deprecation until starlette ships #3498
anyio 4.15 (2026-09-02) deprecates the `anyio.abc.BlockingPortal` alias,
which starlette's TestClient still imports at module load in its latest
release (1.6.0, 2026-08-08). With `filterwarnings = ["error"]` the warning
aborts conftest import: pytest exits with code 4 before running a single
test, on every fresh install since the anyio release. Reproduced locally
by pinning anyio==4.15.1.
Dependencies are current; the fix simply is not published yet: starlette
switched to `anyio.from_thread.BlockingPortal` on master on 2026-09-05
(Kludex/starlette#3498). The ignore is scoped to that one message and
marked to be removed at the first starlette release after 1.6.0.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0113DEa3m359srdD3TtkNm2n
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
…arlette anyio 4.15+ (resolved for py3.11-3.14, but not py3.10's pinned 4.14.2) warns when starlette.testclient accesses the deprecated anyio.abc.BlockingPortal alias at import time. Our filterwarnings = ["error"] turned that into a hard collection error in tests/test_server.py on every non-3.10 CI job. Fixed upstream in Kludex/starlette#3498, but not yet in a starlette release (latest is 1.6.0, predating that merge), so scope a narrow ignore until it ships. Assisted-by: Claude (Anthropic) Co-Authored-By: Claude Sonnet 5 <[email protected]>
…or (#10) * chore(deps): bump mcp lock to 2.2.0 mcp 2.0.0's FuncMetadata.convert_result validates structured_content against a tool's output model unconditionally whenever an output schema exists, with no is_error guard -- returning CallToolResult(is_error=True) with no structured_content raises pydantic.ValidationError instead of failing gracefully. mcp>=2.1.1 adds the `and not result.is_error` guard this repo's upcoming structured-content work depends on. Verified against the mcp SDK source directly (matches the af-filesystem-mcp pilot's finding, af-filesystem-mcp#6. Also, while in here: - Add af-credentials to pixi.toml's [package.run-dependencies]: it was missing even though pyproject.toml declares it a hard dependency (af-credentials[mcp]>=0.2.0), imported directly by auth/broker.py. - Add mypy_path = [src] to pyproject.toml: without it the mypy pre-commit hook can only resolve af_jupyterlab_mcp.* imports when a src/ file happens to be part of the same invocation as the test file being checked. Assisted-by: Claude (Anthropic) EOF ) * refactor(nb_proxy): replace str-sniffing error tunnel with raised exceptions _get_ready_pod_and_token returned tuple[Any, str] | str, and all 16 nb_* tools type-sniffed the result (`if isinstance(result, str): return result`) to detect a failure. That convention has to go before tool return types change to CallToolResult in the next commits, or a real error string would get wrapped as a successful markdown result, silently swallowing the new is_error signal for all 16 proxy tools. - k8s/errors.py: add NotebookNotReadyError. - k8s/proxy.py: add NotebookToolTransportError/NotebookToolUpstreamError; call_notebook_tool now raises instead of returning a formatted error string on transport failure or an upstream is_error result. - tools/nb_proxy.py: _get_ready_pod_and_token now raises NotFoundOrNotYoursError/NotebookNotReadyError instead of returning a string; a new _call_upstream helper centralizes the try/except + hint-attachment logic previously duplicated (with the sniff) across all 16 tool bodies, which now just build their tool_args dict and delegate. format_error itself is unchanged in this commit -- still returns a plain string -- so tool return types and behavior are unchanged; this is purely the tunnel removal ahead of A.1/A.2. - Fixed a latent test-mock bug surfaced by this refactor: tests/k8s/test_proxy.py's _make_tool_result set the wrong attribute (`isError` instead of `is_error`, the real mcp SDK field name), which meant every mocked "success" result was actually truthy on `result.is_error` and silently took the error branch all along. Fixed the mock and added tests that pin call_notebook_tool's success path. Assisted-by: Claude (Anthropic) * feat: structured content, output schemas, annotations, and isError for all 22 tools Rolls out the af-mcp-platform MCP interop uplift pattern (pilot: af-filesystem-mcp#6) to this backend's 22 tools across tools/jupyterlab.py (6) and tools/nb_proxy.py (16). - _helpers.format_error now returns CallToolResult(is_error=True) instead of a bare string, keeping recovery-hint prose byte-for-byte. Since this helper is shared by both tool modules, it had to change atomically with every call site across both files -- see the commit-level test run rather than a smaller per-module split. - Every tool's return annotation becomes Annotated[CallToolResult, Model], publishing an outputSchema while keeping the existing curated markdown text block unchanged; structured_content rides alongside it. - tools/jupyterlab.py: NotebookSummary mirrors exactly the field set _helpers.format_notebook already selects via _SUMMARY_FIELDS -- the structured payload promises no more than the markdown already does, rather than dumping the much richer/irregularly-shaped raw k8s info dict (conditions, events, node_selector, etc.) into a schema. - tools/nb_proxy.py: jupyter-mcp-server (the upstream every nb_* tool proxies to) returns markdown/plain text for every tool, never structured data, so there is no further structure to extract without coupling to its undocumented text format. NbProxyResult is a minimal {"result": <str>} wrapper around that opaque passthrough -- the pragmatic per-tool minimum the interop plan anticipated for this case. _call_upstream (added in the prior commit) is the single place that builds this wrapper, so all 16 tools get it for free. - ToolAnnotations per the interop plan's bucket assignment: 9 read-only (open_world_hint=True -- all reach external k8s/jupyter-mcp-server state), 6 mutating non-destructive, 7 destructive. The three arbitrary-code-execution tools (nb_execute_cell, nb_insert_execute_code_cell, nb_execute_code) get destructive_hint=True even though "execute" isn't literally a delete -- they can mutate anything the kernel can reach. - Added tests/conftest.py's tool_text() fixture (unwraps CallToolResult.content[0].text) so every existing substring assertion keeps working unchanged; added drift-guard tests per module asserting every tool declares annotations + an outputSchema and lands in the right read-only/mutating/destructive bucket; added tests/tools/test_helpers.py for format_error/format_notebook/format_notebook_list directly (this repo had no dedicated helper tests before). - Context/MCPServer/CallToolResult/TextContent/ToolAnnotations and every result model stay runtime imports, never TYPE_CHECKING, per the mcp SDK's eval_str=True signature introspection requirement. Verified: pixi run -e dev lint (ruff + mypy + pylint, all clean) and pixi run -e {py310,py311,py312,py313,py314} test-all all green (162 tests each, filterwarnings=error). Assisted-by: Claude (Anthropic) * test: add registration-layer and wire-level drift guards for all 22 tools Per the interop plan's A.4/verification section: every existing tool test reaches into mcp._tool_manager.list_tools() and calls tool.fn directly, bypassing the mcp SDK's serialization entirely -- none of them would notice a missing annotations/outputSchema on the wire. - TestEveryToolDeclaresAnnotationsAndOutputSchema: registers both tool modules together (mirroring production's _register_all) and asserts every one of the 22 tools declares annotations + an outputSchema. - TestToolsOverTheWire: a real JSON-RPC round trip (initialize -> tools/list -> tools/call) through a built streamable-HTTP ASGI app. Uses a dedicated, unauthenticated app rather than the broker-mode one (which runs stateless_http=True and doesn't do JSON responses) so the round trip isn't entangled with session/auth wiring already covered by TestBrokerModeApp; list_supported_images is exercised for tools/call because it's the one tool that touches neither the broker verifier nor the k8s clients, so no cluster mocking is needed. Verified: pixi run -e dev lint clean; full matrix (py310-py314) all green, 165 tests each. Assisted-by: Claude (Anthropic) * docs: update tool registration pattern and README for structured output + 22 tools CLAUDE.md's "Tool registration pattern" and "Adding a new tool" checklist described the pre-interop-uplift shape (bare str returns, no annotations, single tools/jupyterlab.py module) -- rewritten to the Annotated[CallToolResult, Model] + ToolAnnotations pattern, covering both tools/jupyterlab.py and tools/nb_proxy.py, with the nb_* proxy's NbProxyResult wrapper and exception-based error contract called out explicitly. README.md was stale independent of this work: its "## Tool surface" section documented only 6 of 22 tools and still said the nb_* proxy was "tracked, not yet built" (it shipped in 4ea435f). Rewrote it to list all 22 tools with a read-only/mutating/destructive column, fixed the stale Phase 1/Phase 2 framing, and added nb_proxy.py/_helpers.py/k8s/proxy.py to the project-layout tree. Assisted-by: Claude (Anthropic) * chore(deps): add tbump to dev dependencies af-filesystem-mcp's pixi.toml already has this (the pilot repo); this one was missing it, which would have blocked the version-bump step of this PR's own workflow. No dedicated task -- tbump is run directly via \`pixi run -e dev tbump <new-version>\`. Assisted-by: Claude (Anthropic) * chore: bump version 0.2.0 → 0.2.1 Assisted-by: Claude (Anthropic) * Revert "chore: bump version 0.2.0 → 0.2.1" This reverts commit 553cb8a. * fix(test): ignore deprecated anyio.abc.BlockingPortal warning from starlette anyio 4.15+ (resolved for py3.11-3.14, but not py3.10's pinned 4.14.2) warns when starlette.testclient accesses the deprecated anyio.abc.BlockingPortal alias at import time. Our filterwarnings = ["error"] turned that into a hard collection error in tests/test_server.py on every non-3.10 CI job. Fixed upstream in Kludex/starlette#3498, but not yet in a starlette release (latest is 1.6.0, predating that merge), so scope a narrow ignore until it ships. Assisted-by: Claude (Anthropic) Co-Authored-By: Claude Sonnet 5 <[email protected]> --------- Co-authored-by: Claude Sonnet 5 <[email protected]>
|
@Kludex Would you please consider releasing this as a hotfix when convenient? I’d really appreciate it. Thanks 🙏 |
Tomorrow. I want some stuff in for the next release. |
anyio 4.15 deprecated the anyio.abc.BlockingPortal alias. starlette 1.6.0 testclient.py:53 still reads it, and the expression runs at import time. With filterwarnings = ["error"], tests/conftest.py fails to import and every test module fails to collect. pylint then reports F6401 and six W0621, because pylint-pytest cannot enumerate fixtures through a broken conftest. - filterwarnings ignores that one message. Kludex/starlette#3497 reports the defect and Kludex/starlette#3498 fixed it on master. The comment names the release that retires the line. - Only tests import starlette.testclient, so the web app is unaffected. Major version bumps: - ffmpeg takes >=9.0.1,<10. It supplies the ffprobe and ffmpeg binaries that cat_watcher.detector runs per clip, so it reaches the deploy host through the deploy-update task. - pytest-randomly takes >=5.0.0,<6. It is a dev-only plugin. Formatter output in this diff: - pyproject-fmt 2.29.4 reorders the tables and sorts the good-names and bad-names arrays under [tool.pylint.basic]. Every key keeps its value and its comment. - djlint 1.46.2 splits the offline-storage banner attributes in timeline.html.jinja onto one line each. The class attribute stays whole. Version bumps with no other effect: - Runtime: alembic, httpx2, pydantic, sqlalchemy, uvicorn. - Dev: basedpyright, djlint, pylint, pyproject-fmt, ruff. - Conda: numpy, ultralytics, zizmor. - npm: eslint, globals, stylelint, stylelint-declaration-strict-value, stylelint-plugin-defensive-css, stylelint-plugin-use-baseline. - Transitive: pixi.lock and package-lock.json hold every newest release the constraints allow. Held at the current version on purpose: - arel keeps the git pin. PyPI still ships 0.4.0, which pins starlette==0.*. - pylint-pytest keeps 2.0.0a1. The newest stable release is 1.1.8, and it caps pytest below 9.
The cap was added because anyio 4.15 deprecated an import that starlette's TestClient used. starlette 1.10.0 includes the fix (Kludex/starlette#3498, TestClient now uses anyio.from_thread.BlockingPortal), so the pin is no longer needed. anyio is no longer a direct dev dependency; it is still installed through starlette and httpx. Locks anyio 4.15.1. Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Summary
Replace
anyio.abc.BlockingPortalwithanyio.from_thread.BlockingPortalinTestClient. Importingstarlette.testclientno longer emits aDeprecationWarningon anyio 4.15+.Fixes #3497
Checklist