Skip to content

A terminal file argument re-collects its parent package and forks subpackage collectors, losing conftest fixtures (9.1.x) #15071

Closed as duplicate

Description

@Parad0x-Labs
  • pytest 9.1.0 and 9.1.1 (also verified 9.1.1 is the latest release), CPython 3.12.13, macOS 15 (local) and ubuntu-24.04 GitHub runner
  • minimal reproduction below

A terminal file argument re-collects its parent package and forks subpackage collectors, losing conftest fixtures for sibling items

Reproduction

mkdir -p tests/sub
touch tests/__init__.py tests/sub/__init__.py
cat > tests/conftest.py <<'EOF'
import pytest

@pytest.fixture
def parent_marker():
    return "parent"
EOF
cat > tests/sub/conftest.py <<'EOF'
import pytest

@pytest.fixture
def the_answer():
    return 42
EOF
cat > tests/top1.py <<'EOF'
def test_top1():
    assert True
EOF
cat > tests/top2.py <<'EOF'
def test_top2():
    assert True
EOF
cat > tests/sub/test_uses_fixture.py <<'EOF'
def test_uses_fixture(the_answer):
    assert the_answer == 42
EOF
cat > tests/sub/test_second.py <<'EOF'
def test_second(the_answer):
    assert the_answer == 42
EOF

python -m pytest -q tests/top1.py tests/sub/test_uses_fixture.py tests/top2.py tests/sub/test_second.py
# -> 3 passed, 1 ERROR: fixture 'the_answer' not found (which item errors depends on argument order)
python -m pytest -q tests/top1.py tests/top2.py tests/sub/test_uses_fixture.py tests/sub/test_second.py
# -> 4 passed (no interleaving, no bug)

Any order where a sub-package file argument sits between two top-level file arguments triggers it.

Mechanism (from reading 9.1.1 source and instrumenting)

  1. Session.collect builds each file argument's parent chain. For a terminal file part it calls _collect_one_node(parent, handle_dupes=False) — the collection cache is bypassed for the file's parent, so duplicate file arguments keep working.
  2. That re-collection runs Package.collect() again for the parent (tests), which has no per-path dedup: it creates a fresh Package collector for every subpackage (tests/sub) via pytest_collect_directory, and _collect_one_node stores the fresh report, replacing the cached children.
  3. Conftest fixtures are bound one-shot: FixtureManager.pytest_make_collect_report pops _pending_conftests for the directory path at the first Directory report, binding the fixturedefs to that first collector object.
  4. Items collected under a later duplicate Package then fail FixtureManager._matchfactories, which in 9.1.x matches by node identity first (fixturedef.node in parent_nodes) — the string-baseid fallback never runs because fixturedef.node is not None — producing "fixture not found" for exactly those items.

Instrumentation on a large real suite (one session, ~200 file arguments) counted 7 distinct Package objects for one directory and systematic "fixture not found" errors for the items under the later copies. Absolute vs relative file arguments make no difference (arguments are absolutized internally), and 9.1.1 shows the same behavior as 9.1.0.

Expected behavior

One collector object per directory path per session, so conftest fixturedefs remain visible to every item collected under that directory regardless of argument order.

Workaround in use

A firstresult pytest_collect_directory hook that returns the already-created collector for a path (recorded at pytest_collectstart) prevents the fork.

Thank you for pytest!

Activity

  1. RonnyPfannschmidt commented on Sep 21, 2026

    @RonnyPfannschmidt
    Member

    Duplicate of #14635, already fixed and waiting on a release. Closing.

    Written by Claude Opus 5 via Claude Code for the issue reporter; I prompted it, it did the work, I read it.

    The fix is #14645 (fc8f56bd2, "fix: deduplicate Directory nodes on re-collection to preserve fixture identity"), backported to 9.1.x in #14968. It makes one collector object per directory path per session.

    No released version carries it yet — 9.1.1 predates the merge, which is why the 9.1.1 source matches your analysis. The reproducer in this issue passes unmodified on current main and 9.1.x. Until 9.1.2, the pytest_collect_directory hook or sorting the file arguments both avoid it.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions