Skip to content

Use anyio.from_thread.BlockingPortal in TestClient - #3498

Merged
Kludex merged 4 commits into
Kludex:mainfrom
vyrnsynx:fix-testclient-blockingportal-alias
Sep 5, 2026
Merged

Kludex merged 4 commits into
Kludex:mainfrom
vyrnsynx:fix-testclient-blockingportal-alias

Conversation

@vyrnsynx

@vyrnsynx vyrnsynx commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replace anyio.abc.BlockingPortal with anyio.from_thread.BlockingPortal in TestClient. Importing starlette.testclient no longer emits a DeprecationWarning on anyio 4.15+.

Fixes #3497

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

Review in cubic

anyio 4.15 deprecated anyio.abc.BlockingPortal. The runtime type alias
in testclient.py evaluated that name on import.

Fixes #3497
@codspeed

codspeed Bot commented Sep 3, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 7.44%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 2 regressed benchmarks
✅ 239 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

@Kludex
Kludex enabled auto-merge (squash) September 5, 2026 07:02
@Kludex
Kludex disabled auto-merge September 5, 2026 07:03
@Kludex
Kludex enabled auto-merge (squash) September 5, 2026 07:03
@Kludex
Kludex merged commit bbee894 into Kludex:main Sep 5, 2026
11 checks passed
prasadus92 added a commit to luminik-io/alfred that referenced this pull request Sep 5, 2026
…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.
felixonmars added a commit to felixonmars/archriscv-packages that referenced this pull request Sep 6, 2026
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
hyperrealist added a commit to bluesky/scanspec that referenced this pull request Sep 7, 2026
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
hyperrealist added a commit to bluesky/scanspec that referenced this pull request Sep 7, 2026
* 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]>
wolfgang-aura added a commit to wolfgang-aura/Mailman that referenced this pull request Sep 11, 2026
`--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]>
Upellift99 added a commit to Upellift99/ovispect that referenced this pull request Sep 12, 2026
…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
Upellift99 added a commit to Upellift99/ovispect that referenced this pull request Sep 12, 2026
* 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]>
kratsg added a commit to maniaclab/af-jupyterlab-mcp that referenced this pull request Sep 18, 2026
…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]>
kratsg added a commit to maniaclab/af-jupyterlab-mcp that referenced this pull request Sep 18, 2026
…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]>
@pygarap

pygarap commented Sep 19, 2026

Copy link
Copy Markdown

@Kludex Would you please consider releasing this as a hotfix when convenient? I’d really appreciate it. Thanks 🙏

@Kludex

Kludex commented Sep 19, 2026

Copy link
Copy Markdown
Owner

@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.

rgant added a commit to rgant/cat-watcher that referenced this pull request Sep 20, 2026
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.
hyperrealist added a commit to bluesky/scanspec that referenced this pull request Oct 27, 2026
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]>
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.

testclient imports the deprecated anyio.abc.BlockingPortal alias

3 participants