Skip to content

v2 rewrite (Claude version)#1516

Draft
nicoddemus wants to merge 9 commits into
pytest-dev:mainfrom
nicoddemus:claude-rewrite
Draft

v2 rewrite (Claude version)#1516
nicoddemus wants to merge 9 commits into
pytest-dev:mainfrom
nicoddemus:claude-rewrite

Conversation

@nicoddemus

Copy link
Copy Markdown
Member

No description provided.

nicoddemus and others added 6 commits July 21, 2026 13:43
Purely mechanical extraction ahead of the v2 hook-based rewrite: no
behavior change. plugin.py becomes a thin aggregator that re-exports
hookimpls and fixtures from _hooks, _config, _markers, _runner,
_collection, _fixtures, and _dispatch, so later commits' diffs are
reviewable in isolation from this reorganization.

Verified byte-for-byte behavior preservation by running the full test
suite before and after: identical 283 passed / 12 failed (pre-existing,
environment-related) / 3 skipped in both cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes PytestAsyncioFunction and its four subclasses (Coroutine,
AsyncGenerator, AsyncStaticMethod, AsyncHypothesisTest). Async tests are
now tagged at collection time via pytest.Stash (asyncio_test_key,
loop_scope_key) on plain pytest.Function items instead of swapped for a
custom Item subclass, and dispatched by monkeypatching the coroutine
directly inside pytest_pyfunc_call rather than through an overridden
runtest(). is_async_test() becomes a stash lookup returning plain bool
instead of an isinstance check returning TypeIs[PytestAsyncioFunction].

The async-generator-test rejection, staticmethod-coroutine support, and
Hypothesis (@given) integration (including its deferred version-compat
check, now run from a new pytest_runtest_setup hookimpl) are preserved
behaviorally, just relocated out of the subclass hierarchy into shared
classification helpers.

Runner/event-loop lifecycle management is untouched: still internal,
dynamically-generated per-scope fixtures, reusing pytest's own fixture-
cache teardown-ordering machinery rather than hand-rolling scope-
transition tracking.

One correctness-critical detail surfaced while porting the loop_factory
eager-fetch optimization: v1's PytestAsyncioFunction.setup() override ran
code between ancestor-scope setup and this item's own _fillfixtures(), a
seam that no longer exists without Item subclassing. Calling
request.getfixturevalue() from a standalone pytest_runtest_setup hookimpl
(before or after SetupState's own setup) trips pytest 9.1's newer
fixture-request-lifecycle validation and breaks fixture resolution
broadly. The fix instead controls _fillfixtures()'s own (in-order)
resolution sequence: the side-effect-free _asyncio_loop_factory fixture
is inserted first in the item's fixturenames so a loop-factory variant
change invalidates stale same-scope async fixtures before they're
resolved, while the scoped runner fixture -- which does have side effects
(it becomes the current asyncio event loop on creation) -- stays appended
last, matching v1's original position.

Full test suite verified before and after: identical 288 passed / 7
failed (pre-existing, environment-related -- see below) / 3 skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The autouse, user-overridable event_loop_policy fixture is gone: scoped
runner fixtures now call _get_event_loop_policy() directly instead of
taking it as a fixture parameter, and pytest_fixture_setup no longer has
a deprecation-warning branch for user overrides (there's nothing left to
override).

Since this is a common pattern users likely still have in their conftest
files (the standard v1 uvloop-customization idiom), silently turning it
into inert dead code would be a bad upgrade experience -- the fixture
would keep existing syntactically but quietly stop having any effect. A
new pytest_collection_finish hook (_usage_checks.py) scans for any
fixture literally named "event_loop_policy" after collection and raises
pytest.UsageError pointing at the pytest_asyncio_loop_factories hook as
the replacement.

Removed the ~13 now-broken tests across the five loop-scope marker test
files that exercised event_loop_policy overrides (superseded by
tests/test_loop_factory_parametrization.py) and added
tests/test_event_loop_policy_removed.py covering the new UsageError.
Deleted the two event_loop_policy example docs and the multiple_loops
how-to guide (entirely event_loop_policy-based, fully superseded by the
existing hook-based loop_factory guides); trimmed the fixture-based
section from the uvloop guide.

Full suite verified: 275 passed / 7 failed (same pre-existing,
environment-related failures as before) / 3 skipped, plus docs/ (26
passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pytest.mark.asyncio(scope=...) (the deprecated alias for loop_scope=)
is no longer accepted. Since v2 is already a breaking release, this
already-deprecated kwarg doesn't need to carry through another major
version -- scope= now falls through to the same generic "accepts only
keyword arguments 'loop_scope' and 'loop_factories'" ValueError as any
other unrecognized kwarg.

Updated tests/markers/test_function_scope.py: the "both scope and
loop_scope present" test keeps its errors=1 outcome but now asserts the
generic unrecognized-kwarg message instead of the old duplicate-
definition error; the "scope alone" test changes from a passing/warning
outcome to an error outcome.

Full suite verified: 276 passed / 6 failed (pre-existing,
environment-related) / 3 skipped, docs/ 26 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ytest-dev#1514)

pytest-asyncio's loop_scope is a concept independent of pytest's own
fixture cache scope (a function-cache-scoped fixture can declare
loop_scope="session"), so pytest's built-in ScopeMismatch check gives no
protection against a test and a fixture it depends on actually running
on different event loops -- which can silently break asyncio.Future,
Task, or Lock objects bound to the loop they were created on.

New _mismatch.py computes, once per item at collection time, the full
transitive closure of async-owned fixtures whose effective loop_scope
differs from the test's own (walking pytest's own deduplicated
names_closure, so a diamond-shaped fixture graph can't produce duplicate
warnings for the same fixture). Detection is direction-agnostic: a
narrower test consuming a wider fixture and a narrower fixture consumed
by a wider test are both flagged, since either can leave asyncio objects
bound to a loop that's already gone.

The warning is computed eagerly at collection time but only *emitted*
from pytest_pyfunc_call at actual test-run time, not from the collection
hookwrapper where it's computed: this repo's own filterwarnings=["error"]
would otherwise turn a collection-time warning into a collection error
that can abort a whole module and can't be locally silenced with
pytest.warns() inside a test, since the test hasn't started yet.

_owns_fixture and _effective_fixture_loop_scope, previously inline in
_fixtures.py's pytest_fixture_setup, are now shared helpers so "what we
wrap" and "what we compare" can never drift apart.

As expected, this surfaces real (mostly deliberate, a couple incidental)
mismatches already present in this repo's own test suite -- 15 test
functions needed pytest.mark.filterwarnings/-W default treatment for
scope combinations they intentionally exercise. This is the intended
effect of closing pytest-dev#1514, not a regression: it's exactly the class of bug
the warning exists to catch.

Full suite verified: 283 passed / 6 failed (pre-existing,
environment-related) / 3 skipped, docs/ 26 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New docs/how-to-guides/migrate_from_1_x.rst, matching the structure of
the existing migrate_from_0_21/migrate_from_0_23 guides: covers the
event_loop_policy fixture removal, the scope= marker kwarg removal, the
new PytestAsyncioLoopScopeMismatchWarning (flagged as likely to surface
pre-existing bugs in bulk on first upgrade), and is_async_test's return
type change from TypeIs to plain bool.

Five towncrier fragments in changelog.d/ covering the removed/added/
changed/downstream categories for this release. Validated with
`towncrier build --draft` and a full `sphinx-build -W` (warnings as
errors) run -- both clean, confirming no broken toctree entries or rst
syntax issues across the new and edited pages.

Also tightened docs/reference/warnings.rst to use the same plain
double-backtick/asterisk style as the rest of the docs instead of Sphinx
:class: cross-reference roles, since this project's conf.py has neither
autodoc nor intersphinx configured to resolve them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nicoddemus and others added 3 commits July 21, 2026 13:58
Use Literal instead of str/bool for the finite set of category names
and monkeypatch-target attribute names, so a typo or new category
mismatches with the callers' string comparisons (e.g. category ==
"asyncgen") get caught statically instead of silently doing nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- shed: reformat two new test files and a doc code-block to match the
  repo's established style (collapsed makepyfile/makeconftest calls,
  blank line between import groups).
- mypy: _effective_fixture_loop_scope's or-chain of Any/str values
  needed an explicit cast to the Literal _ScopeName it's declared to
  return. _is_hypothesis_wrapped_coroutine accessed .hypothesis.inner_test
  directly on an object-typed param instead of via getattr. The
  hookwrapper in _dispatch.py had a bare mid-function `return` where v1
  always used `return None`, which mypy treats differently inside a
  generator function. _runner.py's five scoped-runner fixtures were only
  ever bound via `globals()[f"_{scope}_scoped_runner"] = ...` in a loop,
  invisible to static analysis; replaced with explicit assignments per
  scope, which plugin.py now needs anyway since -- unlike v1, which only
  ever looked these fixtures up dynamically by string -- the module-split
  aggregator imports them by name for pytest's fixture discovery.

Verified: full pre-commit run -a clean, full test suite unchanged (283
passed / 6 pre-existing environment-related failures / 3 skipped), docs/
26 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.19409% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.63%. Comparing base (054a52d) to head (40c361b).

Files with missing lines Patch % Lines
pytest_asyncio/_runner.py 87.05% 9 Missing and 2 partials ⚠️
pytest_asyncio/_collection.py 94.49% 3 Missing and 3 partials ⚠️
pytest_asyncio/_fixtures.py 96.45% 4 Missing and 1 partial ⚠️
pytest_asyncio/_config.py 93.54% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1516      +/-   ##
==========================================
+ Coverage   94.50%   94.63%   +0.12%     
==========================================
  Files           2       11       +9     
  Lines         510      578      +68     
  Branches       62       70       +8     
==========================================
+ Hits          482      547      +65     
- Misses         22       23       +1     
- Partials        6        8       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants