Skip to content

Commit 2ffa1e0

Browse files
authored
Fix collection issue in Pytest 9 with overridden fixtures (#376)
1 parent 6b8c371 commit 2ffa1e0

File tree

5 files changed

+118
-4
lines changed

5 files changed

+118
-4
lines changed

docs/changelog.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Changelog
22

3-
### 3.10.0 - (in progress) New `with_case_tags` decorator
3+
### 3.10.0 - (in progress) New `with_case_tags` decorator + pytest 9 compatibility
44

5+
- Fixed an issue with `pytest 9` related to the fixture closure building fixes
6+
[pytest-dev/pytest#13789](https://github.com/pytest-dev/pytest/pull/13789),
7+
solving [pytest-dev/pytest#13773](https://github.com/pytest-dev/pytest/issues/13773).
8+
Fixed [#374](https://github.com/smarie/python-pytest-cases/issues/374). PR
9+
[#376](https://github.com/smarie/python-pytest-cases/pull/376) by [jammer87](https://github.com/jammer87).
510
- Added the `with_case_tags` decorator for applying common tags to all cases
611
defined in a case class. Fixes [#351](https://github.com/smarie/python-pytest-cases/issues/351).
712
PR [#361](https://github.com/smarie/python-pytest-cases/pull/361)

noxfile.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,32 @@ class Folders:
5353
ENVS = {
5454
# python 3.14
5555
(PY314, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
56+
(PY314, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}},
5657
(PY314, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
5758
(PY314, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
5859
# python 3.13
5960
(PY313, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
61+
(PY313, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}},
6062
(PY313, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
6163
(PY313, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
6264
# python 3.12
6365
(PY312, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
66+
(PY312, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}},
6467
(PY312, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
6568
(PY312, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
6669
# python 3.11
6770
# We'll run 'pytest-latest' this last for coverage
71+
(PY311, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}},
6872
(PY311, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
6973
(PY311, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
7074
# python 3.10
7175
(PY310, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
76+
(PY310, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}},
7277
(PY310, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
7378
(PY310, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
7479
# python 3.9
7580
(PY39, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
81+
(PY39, "pytest8.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<9"}},
7682
(PY39, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
7783
(PY39, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
7884
# IMPORTANT: this should be last so that the folder docs/reports is not deleted afterwards

src/pytest_cases/common_pytest_marks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0')
4747
PYTEST81_OR_GREATER = PYTEST_VERSION >= Version('8.1.0')
4848
PYTEST84_OR_GREATER = PYTEST_VERSION >= Version('8.4.0')
49+
PYTEST9_OR_GREATER = PYTEST_VERSION >= Version('9.0.0')
4950

5051

5152
def get_param_argnames_as_list(argnames):

src/pytest_cases/plugin.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828

2929
from .common_mini_six import string_types
3030
from .common_pytest_lazy_values import get_lazy_args
31-
from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER, PYTEST8_OR_GREATER
31+
from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER, \
32+
PYTEST8_OR_GREATER, PYTEST9_OR_GREATER
3233
from .common_pytest import get_pytest_nodeid, get_pytest_function_scopeval, is_function_node, get_param_names, \
3334
get_param_argnames_as_list, has_function_scope, set_callspec_arg_scope_to_function, in_callspec_explicit_args
3435

@@ -334,8 +335,18 @@ def _build_closure(self,
334335
# normal fixture
335336
self.add_required_fixture(fixname, fixturedefs)
336337

337-
# add all dependencies in the to do list
338-
dependencies = _fixdef.argnames
338+
# add all dependencies, accounting for overrides
339+
if PYTEST9_OR_GREATER:
340+
dependencies = []
341+
for _fixture_or_overridden in reversed(fixturedefs):
342+
dependencies = list(_fixture_or_overridden.argnames) + dependencies
343+
# If there's an override and doesn't depend on the overridden fixture,
344+
# ignore remaining definitions
345+
if fixname not in _fixture_or_overridden.argnames:
346+
break
347+
else:
348+
dependencies = _fixdef.argnames
349+
339350
# - append: was pytest default
340351
# pending_fixture_names += dependencies
341352
# - prepend: makes much more sense
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
OVERRIDDEN_FIXTURES_TEST_FILE = """
2+
import pytest
3+
4+
@pytest.fixture
5+
def db(): pass
6+
7+
@pytest.fixture
8+
def app(db): pass
9+
10+
11+
# See https://github.com/pytest-dev/pytest/issues/13773
12+
# Issue occurred in collection with Pytest 9+
13+
14+
15+
class TestOverrideWithParent:
16+
# Overrides module-level app, doesn't request `db` directly, only transitively.
17+
@pytest.fixture
18+
def app(self, app): pass
19+
20+
def test_something(self, app): pass
21+
22+
23+
24+
class TestOverrideWithoutParent:
25+
# Overrides module-level app, doesn't request `db` at all.
26+
@pytest.fixture
27+
def app(self): pass
28+
29+
def test_something(self, app): pass
30+
"""
31+
32+
33+
def test_overridden_fixtures(pytester):
34+
pytester.makepyfile(OVERRIDDEN_FIXTURES_TEST_FILE)
35+
result = pytester.runpytest()
36+
result.assert_outcomes(passed=2)
37+
38+
39+
# Using union fixtures.
40+
OVERRIDDEN_UNION_FIXTURES_TEST_FILE = """
41+
import pytest
42+
from pytest_cases import parametrize, parametrize_with_cases, case, fixture
43+
44+
@fixture
45+
def db(): pass
46+
47+
@fixture
48+
def app(db): pass
49+
50+
def case_hello():
51+
return "hello !"
52+
53+
@fixture
54+
def surname():
55+
return "joe"
56+
57+
@fixture
58+
@parametrize("_name", ["you", "earthling"])
59+
def name(_name, surname, app):
60+
return f"{_name} {surname}"
61+
62+
@case(id="hello_fixture")
63+
def case_basic3(name):
64+
return "hello, %s !" % name
65+
66+
67+
class TestOverrideWithParent:
68+
# Overrides module-level name, doesn't request `name` directly, only transitively.
69+
@fixture
70+
def name(self, name):
71+
return "overridden %s" % name
72+
73+
@parametrize_with_cases("msg", cases=".")
74+
def test_something(self, msg): pass
75+
76+
class TestOverrideWithoutParent:
77+
# Overrides module-level name, doesn't request name at all
78+
@fixture
79+
@parametrize("_name", ["hi", "martian"])
80+
def name(self, _name):
81+
return _name
82+
83+
@parametrize_with_cases("msg", cases=".")
84+
def test_something(self, msg): pass
85+
"""
86+
87+
88+
def test_overridden_unions(pytester):
89+
pytester.makepyfile(OVERRIDDEN_UNION_FIXTURES_TEST_FILE)
90+
result = pytester.runpytest()
91+
result.assert_outcomes(passed=6)

0 commit comments

Comments
 (0)