From a9fb5d23b86ffefc8b342545f23db2bb86d3264b Mon Sep 17 00:00:00 2001 From: Bryan Borg <71280324+jammer87@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:32:28 -0600 Subject: [PATCH 1/4] Fixed collection error with overridden fixtures --- noxfile.py | 6 ++ src/pytest_cases/common_pytest_marks.py | 1 + src/pytest_cases/plugin.py | 18 ++++- tests/cases/issues/test_issue_374.py | 91 +++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 tests/cases/issues/test_issue_374.py diff --git a/noxfile.py b/noxfile.py index 410dd696..78c77440 100644 --- a/noxfile.py +++ b/noxfile.py @@ -53,26 +53,32 @@ class Folders: ENVS = { # python 3.14 (PY314, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}}, + (PY314, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}}, (PY314, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}}, (PY314, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}}, # python 3.13 (PY313, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}}, + (PY313, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}}, (PY313, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}}, (PY313, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}}, # python 3.12 (PY312, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}}, + (PY312, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}}, (PY312, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}}, (PY312, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}}, # python 3.11 # We'll run 'pytest-latest' this last for coverage + (PY311, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}}, (PY311, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}}, (PY311, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}}, # python 3.10 (PY310, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}}, + (PY310, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}}, (PY310, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}}, (PY310, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}}, # python 3.9 (PY39, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}}, + (PY39, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}}, (PY39, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}}, (PY39, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}}, # IMPORTANT: this should be last so that the folder docs/reports is not deleted afterwards diff --git a/src/pytest_cases/common_pytest_marks.py b/src/pytest_cases/common_pytest_marks.py index 74ce1b5b..6fa33b32 100644 --- a/src/pytest_cases/common_pytest_marks.py +++ b/src/pytest_cases/common_pytest_marks.py @@ -46,6 +46,7 @@ PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0') PYTEST81_OR_GREATER = PYTEST_VERSION >= Version('8.1.0') PYTEST84_OR_GREATER = PYTEST_VERSION >= Version('8.4.0') +PYTEST9_OR_GREATER = PYTEST_VERSION >= Version('9.0.0') def get_param_argnames_as_list(argnames): diff --git a/src/pytest_cases/plugin.py b/src/pytest_cases/plugin.py index 907ebb1d..303949ef 100644 --- a/src/pytest_cases/plugin.py +++ b/src/pytest_cases/plugin.py @@ -28,7 +28,8 @@ from .common_mini_six import string_types from .common_pytest_lazy_values import get_lazy_args -from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER, PYTEST8_OR_GREATER +from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER, \ + PYTEST8_OR_GREATER, PYTEST9_OR_GREATER from .common_pytest import get_pytest_nodeid, get_pytest_function_scopeval, is_function_node, get_param_names, \ get_param_argnames_as_list, has_function_scope, set_callspec_arg_scope_to_function, in_callspec_explicit_args @@ -334,8 +335,19 @@ def _build_closure(self, # normal fixture self.add_required_fixture(fixname, fixturedefs) - # add all dependencies in the to do list - dependencies = _fixdef.argnames + # add all dependencies in the to the list, accounting for overrides + if PYTEST9_OR_GREATER: + dependencies_set = set() + for _fixture_or_overridden in reversed(fixturedefs): + dependencies_set.update(_fixture_or_overridden.argnames) + # If there's an override and doesn't depend on the overridden fixture, + # ignore remaining definitions + if fixname not in _fixture_or_overridden.argnames: + break + dependencies = list(dependencies_set) + else: + dependencies = _fixdef.argnames + # - append: was pytest default # pending_fixture_names += dependencies # - prepend: makes much more sense diff --git a/tests/cases/issues/test_issue_374.py b/tests/cases/issues/test_issue_374.py new file mode 100644 index 00000000..20da941d --- /dev/null +++ b/tests/cases/issues/test_issue_374.py @@ -0,0 +1,91 @@ +OVERRIDDEN_FIXTURES_TEST_FILE = """ +import pytest + +@pytest.fixture +def db(): pass + +@pytest.fixture +def app(db): pass + + +# See https://github.com/pytest-dev/pytest/issues/13773 +# Issue occurred in collection with Pytest 9+ + + +class TestOverrideWithParent: + # Overrides module-level app, doesn't request `db` directly, only transitively. + @pytest.fixture + def app(self, app): pass + + def test_something(self, app): pass + + + +class TestOverrideWithoutParent: + # Overrides module-level app, doesn't request `db` at all. + @pytest.fixture + def app(self): pass + + def test_something(self, app): pass +""" + + +def test_overridden_fixtures(pytester): + pytester.makepyfile(OVERRIDDEN_FIXTURES_TEST_FILE) + result = pytester.runpytest("-p", "pytest_cases.plugin") + result.assert_outcomes(passed=2) + + +# Using union fixtures. +OVERRIDDEN_UNION_FIXTURES_TEST_FILE = """ +import pytest +from pytest_cases import parametrize, parametrize_with_cases, case, fixture + +@fixture +def db(): pass + +@fixture +def app(db): pass + +def case_hello(): + return "hello !" + +@fixture +def surname(): + return "joe" + +@fixture +@parametrize("_name", ["you", "earthling"]) +def name(_name, surname, app): + return f"{_name} {surname}" + +@case(id="hello_fixture") +def case_basic3(name): + return "hello, %s !" % name + + +class TestOverrideWithParent: + # Overrides module-level name, doesn't request `name` directly, only transitively. + @fixture + def name(self, name): + return "overridden %s" % name + + @parametrize_with_cases("msg", cases=".") + def test_something(self, msg): pass + +class TestOverrideWithoutParent: + # Overrides module-level name, doesn't request name at all + @fixture + @parametrize("_name", ["hi", "martian"]) + def name(self, _name): + return _name + + @parametrize_with_cases("msg", cases=".") + def test_something(self, msg): pass +""" + + +def test_overridden_unions(pytester): + pytester.makepyfile(OVERRIDDEN_UNION_FIXTURES_TEST_FILE) + result = pytester.runpytest("-p", "pytest_cases.plugin") + result.assert_outcomes(passed=6) From 896400a51559279038e6ece4f37568fae24f5190 Mon Sep 17 00:00:00 2001 From: Bryan Borg <71280324+jammer87@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:53:09 -0600 Subject: [PATCH 2/4] comment fix --- src/pytest_cases/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytest_cases/plugin.py b/src/pytest_cases/plugin.py index 303949ef..f5e1c626 100644 --- a/src/pytest_cases/plugin.py +++ b/src/pytest_cases/plugin.py @@ -335,7 +335,7 @@ def _build_closure(self, # normal fixture self.add_required_fixture(fixname, fixturedefs) - # add all dependencies in the to the list, accounting for overrides + # add all dependencies, accounting for overrides if PYTEST9_OR_GREATER: dependencies_set = set() for _fixture_or_overridden in reversed(fixturedefs): From 88263b9db95d7deb10fa3df8d10db01482e794c7 Mon Sep 17 00:00:00 2001 From: Bryan Borg <71280324+jammer87@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:07:53 -0600 Subject: [PATCH 3/4] Check if this fixes the plugin already loaded issue --- tests/cases/issues/test_issue_374.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cases/issues/test_issue_374.py b/tests/cases/issues/test_issue_374.py index 20da941d..2fe8073f 100644 --- a/tests/cases/issues/test_issue_374.py +++ b/tests/cases/issues/test_issue_374.py @@ -32,7 +32,7 @@ def test_something(self, app): pass def test_overridden_fixtures(pytester): pytester.makepyfile(OVERRIDDEN_FIXTURES_TEST_FILE) - result = pytester.runpytest("-p", "pytest_cases.plugin") + result = pytester.runpytest() result.assert_outcomes(passed=2) @@ -87,5 +87,5 @@ def test_something(self, msg): pass def test_overridden_unions(pytester): pytester.makepyfile(OVERRIDDEN_UNION_FIXTURES_TEST_FILE) - result = pytester.runpytest("-p", "pytest_cases.plugin") + result = pytester.runpytest() result.assert_outcomes(passed=6) From 6630068b856e05242e95617397e5636cce996b49 Mon Sep 17 00:00:00 2001 From: Bryan Borg <71280324+jammer87@users.noreply.github.com> Date: Thu, 8 Jan 2026 07:19:48 -0600 Subject: [PATCH 4/4] Remove set use to fix node name failures --- src/pytest_cases/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pytest_cases/plugin.py b/src/pytest_cases/plugin.py index f5e1c626..ab14ee3b 100644 --- a/src/pytest_cases/plugin.py +++ b/src/pytest_cases/plugin.py @@ -337,14 +337,13 @@ def _build_closure(self, # add all dependencies, accounting for overrides if PYTEST9_OR_GREATER: - dependencies_set = set() + dependencies = [] for _fixture_or_overridden in reversed(fixturedefs): - dependencies_set.update(_fixture_or_overridden.argnames) + dependencies = list(_fixture_or_overridden.argnames) + dependencies # If there's an override and doesn't depend on the overridden fixture, # ignore remaining definitions if fixname not in _fixture_or_overridden.argnames: break - dependencies = list(dependencies_set) else: dependencies = _fixdef.argnames