Skip to content

Commit d489247

Browse files
0xDEC0DEnisimondnicoddemusbluetech
authored
Fix caching of parameterized fixtures (#12600)
The fix for Issue #6541 caused regression where cache hits became cache misses, unexpectedly. Fixes #6962 --------- Co-authored-by: Nicolas Simonds <[email protected]> Co-authored-by: Bruno Oliveira <[email protected]> Co-authored-by: Ran Benita <[email protected]>
1 parent 400b22d commit d489247

File tree

4 files changed

+45
-4
lines changed

4 files changed

+45
-4
lines changed

Diff for: AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ Nicholas Devenish
306306
Nicholas Murphy
307307
Niclas Olofsson
308308
Nicolas Delaby
309+
Nicolas Simonds
309310
Nico Vidal
310311
Nikolay Kondratyev
311312
Nipunn Koorapati

Diff for: changelog/6962.bugfix.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Parametrization parameters are now compared using `==` instead of `is` (`is` is still used as a fallback if the parameter does not support `==`).
2+
This fixes use of parameters such as lists, which have a different `id` but compare equal, causing fixtures to be re-computed instead of being cached.

Diff for: src/_pytest/fixtures.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -1053,12 +1053,18 @@ def execute(self, request: SubRequest) -> FixtureValue:
10531053
requested_fixtures_that_should_finalize_us.append(fixturedef)
10541054

10551055
# Check for (and return) cached value/exception.
1056-
my_cache_key = self.cache_key(request)
10571056
if self.cached_result is not None:
1057+
request_cache_key = self.cache_key(request)
10581058
cache_key = self.cached_result[1]
1059-
# note: comparison with `==` can fail (or be expensive) for e.g.
1060-
# numpy arrays (#6497).
1061-
if my_cache_key is cache_key:
1059+
try:
1060+
# Attempt to make a normal == check: this might fail for objects
1061+
# which do not implement the standard comparison (like numpy arrays -- #6497).
1062+
cache_hit = bool(request_cache_key == cache_key)
1063+
except (ValueError, RuntimeError):
1064+
# If the comparison raises, use 'is' as fallback.
1065+
cache_hit = request_cache_key is cache_key
1066+
1067+
if cache_hit:
10621068
if self.cached_result[2] is not None:
10631069
exc, exc_tb = self.cached_result[2]
10641070
raise exc.with_traceback(exc_tb)

Diff for: testing/python/fixtures.py

+32
Original file line numberDiff line numberDiff line change
@@ -1557,6 +1557,38 @@ def test_printer_2(self):
15571557
result = pytester.runpytest()
15581558
result.stdout.fnmatch_lines(["* 2 passed in *"])
15591559

1560+
def test_parameterized_fixture_caching(self, pytester: Pytester) -> None:
1561+
"""Regression test for #12600."""
1562+
pytester.makepyfile(
1563+
"""
1564+
import pytest
1565+
from itertools import count
1566+
1567+
CACHE_MISSES = count(0)
1568+
1569+
def pytest_generate_tests(metafunc):
1570+
if "my_fixture" in metafunc.fixturenames:
1571+
# Use unique objects for parametrization (as opposed to small strings
1572+
# and small integers which are singletons).
1573+
metafunc.parametrize("my_fixture", [[1], [2]], indirect=True)
1574+
1575+
@pytest.fixture(scope='session')
1576+
def my_fixture(request):
1577+
next(CACHE_MISSES)
1578+
1579+
def test1(my_fixture):
1580+
pass
1581+
1582+
def test2(my_fixture):
1583+
pass
1584+
1585+
def teardown_module():
1586+
assert next(CACHE_MISSES) == 2
1587+
"""
1588+
)
1589+
result = pytester.runpytest()
1590+
result.stdout.no_fnmatch_line("* ERROR at teardown *")
1591+
15601592

15611593
class TestFixtureManagerParseFactories:
15621594
@pytest.fixture

0 commit comments

Comments
 (0)