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..ab14ee3b 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,18 @@ 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, accounting for overrides + if PYTEST9_OR_GREATER: + dependencies = [] + for _fixture_or_overridden in reversed(fixturedefs): + 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 + 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..2fe8073f --- /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() + 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() + result.assert_outcomes(passed=6)