Skip to content

Fix loop factory parametrization for sync fixtures - #1553

Open
TLNing260310 wants to merge 3 commits into
pytest-dev:mainfrom
TLNing260310:fix-1501-sync-fixture-loop-factories
Open

Fix loop factory parametrization for sync fixtures#1553
TLNing260310 wants to merge 3 commits into
pytest-dev:mainfrom
TLNing260310:fix-1501-sync-fixture-loop-factories

Conversation

@TLNing260310

Copy link
Copy Markdown

Fixes #1501.

Summary

  • Parametrize synchronous tests over configured loop factories when their static fixture graph contains a pytest-asyncio-managed fixture.
  • Run the plugin's generation hook after pytest and user parametrization hooks so direct parameters correctly shadow same-named fixtures while indirect parameters keep using their fixture definitions.
  • Use the widest effective managed-fixture loop scope and resolve the loop-factory parameter before dependent fixtures inspect their caches.
  • Cover fixture overrides, transitive dependencies, autouse fixtures, auto mode, teardown order, and direct/indirect parametrization; document the static-closure boundary.

Why

Previously only async test items requested the parametrized loop-factory fixture. A synchronous test consuming a managed async fixture therefore requested an unparametrized/default factory, changed the factory cache key, and could tear down shared fixtures at a sync/async test boundary. Parametrizing the affected synchronous test items keeps the factory cache key and fixture lifetime consistent.

Validation

  • tests/test_loop_factory_parametrization.py: 55 passed with pytest 8.4.0
  • tests/test_loop_factory_parametrization.py: 55 passed with pytest 9.1.1
  • Broader pytest 8.4 run: 306 passed, 1 unrelated Windows nested-pytest test deselected; the local 8.4 environment did not include Hypothesis
  • Broader pytest 9.1 run: 308 passed, 3 skipped, 1 unrelated Windows nested-pytest test deselected
  • ruff check pytest_asyncio tests
  • pyright pytest_asyncio tests

The documentation build was not run locally because Sphinx was not installed in the test environment.

@codecov-commenter

codecov-commenter commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.83%. Comparing base (40342fa) to head (d86d02c).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1553      +/-   ##
==========================================
+ Coverage   94.50%   94.83%   +0.32%     
==========================================
  Files           2        2              
  Lines         510      542      +32     
  Branches       62       69       +7     
==========================================
+ Hits          482      514      +32     
  Misses         22       22              
  Partials        6        6              

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

@tjkuson tjkuson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR, but as-written it duplicates, coerces, and undermines pytest's internals in a way that's error-prone and difficult to maintain.

Comment thread pytest_asyncio/plugin.py Outdated
# before their values are supplied to a synchronous test.
fixtureinfo = metafunc.definition._fixtureinfo
if _asyncio_loop_factory.__name__ not in fixtureinfo.initialnames:
object.__setattr__(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not be manipulating pytest internals like this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d86d02c. I removed the object.setattr call and no longer mutate FuncFixtureInfo.initialnames. After pytest has created and pruned the Function item, the existing collection wrapper places the already-parametrized internal factory fixture first in the item's fixture resolution order, so its cache key changes before managed fixtures are read.

Comment thread pytest_asyncio/plugin.py Outdated
@pytest.hookimpl(tryfirst=True)
# Direct parametrization replaces same-named fixtures during this hook. Run after
# pytest and user hooks so the resolved fixture graph reflects those replacements.
@pytest.hookimpl(trylast=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the node IDs which is breaking for users that match on test names.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d86d02c. The original async pytest_generate_tests(tryfirst=True) ordering is restored. Sync fixture detection now uses a separate post-yield wrapper, so existing async IDs retain the factory-user_parameter order. I added collect-only regression coverage for both the preserved async order and the new sync user_parameter-factory order.

Comment thread pytest_asyncio/plugin.py
return None


def _get_managed_fixture_loop_scope(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reimplements pytest's fixture traversal system in a way that's error-prone is difficult to maintain.

@TLNing260310 TLNing260310 Sep 1, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d86d02c. The recursive fixture traversal, FixtureManager fallback, and override-chain bookkeeping have been removed. Sync detection now only scans the pytest-computed metafunc.fixturenames closure and reads the active fixturedefs[-1]. The docs explicitly limit the behavior to dependencies represented in the static closure rather than reimplementing traversal for dynamic or same-name override cases.

@TLNing260310

Copy link
Copy Markdown
Author

Thanks for the review. I reworked the implementation in d86d02c around the three concerns: the custom recursive fixture traversal and FixtureManager fallback are gone; FuncFixtureInfo.initialnames is no longer mutated; and the original tryfirst ordering for async tests is restored with collect-only node-ID regression tests. Sync tests now use a separate post-yield generate wrapper, scan only the pytest-computed static fixture closure, and resolve the factory cache key before managed fixtures. The documented boundary is intentionally the closure pytest exposes: dynamic request.getfixturevalue dependencies and hidden same-name override chains are not rediscovered. The only remaining private access is a shallow, read-only lookup of the active FixtureDef through Metafunc._arg2fixturedefs because pytest 8.4-9.1 has no public equivalent. All CI checks pass, including pytest main, Python 3.10-3.14/3.14t on Windows and Linux, docs, lint, and packaging. Codecov reports all modified coverable lines covered. I would appreciate another review.

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.

Loop factory parametrization tears down async fixtures used by sync tests

3 participants