Skip to content

Commit 27f442d

Browse files
committed
no cache fixtures POC
1 parent 9515dfa commit 27f442d

File tree

2 files changed

+138
-3
lines changed

2 files changed

+138
-3
lines changed

src/_pytest/fixtures.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,12 @@ def getfixturevalue(self, argname: str) -> Any:
534534
f'The fixture value for "{argname}" is not available. '
535535
"This can happen when the fixture has already been torn down."
536536
)
537+
538+
if (isinstance(fixturedef, FixtureDef)
539+
and fixturedef is not None
540+
and fixturedef.use_cache is False):
541+
self._fixture_defs.pop(argname)
542+
537543
return fixturedef.cached_result[0]
538544

539545
def _iter_chain(self) -> Iterator[SubRequest]:
@@ -614,9 +620,15 @@ def _get_active_fixturedef(
614620
)
615621

616622
# Make sure the fixture value is cached, running it if it isn't
617-
fixturedef.execute(request=subrequest)
623+
try:
624+
fixturedef.execute(request=subrequest)
625+
self._fixture_defs[argname] = fixturedef
626+
finally:
627+
for arg_name in fixturedef.argnames:
628+
arg_fixture = self._fixture_defs.get(arg_name)
629+
if arg_fixture is not None and arg_fixture.use_cache is not True:
630+
self._fixture_defs.pop(arg_name)
618631

619-
self._fixture_defs[argname] = fixturedef
620632
return fixturedef
621633

622634
def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None:
@@ -957,6 +969,7 @@ def __init__(
957969
scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None,
958970
params: Sequence[object] | None,
959971
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
972+
use_cache: bool = True,
960973
*,
961974
_ispytest: bool = False,
962975
) -> None:
@@ -1004,6 +1017,7 @@ def __init__(
10041017
# Can change if the fixture is executed with different parameters.
10051018
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
10061019
self._finalizers: Final[list[Callable[[], object]]] = []
1020+
self.use_cache = use_cache
10071021

10081022
@property
10091023
def scope(self) -> _ScopeName:
@@ -1054,7 +1068,7 @@ def execute(self, request: SubRequest) -> FixtureValue:
10541068
requested_fixtures_that_should_finalize_us.append(fixturedef)
10551069

10561070
# Check for (and return) cached value/exception.
1057-
if self.cached_result is not None:
1071+
if self.cached_result is not None and self.use_cache:
10581072
request_cache_key = self.cache_key(request)
10591073
cache_key = self.cached_result[1]
10601074
try:
@@ -1183,6 +1197,7 @@ class FixtureFunctionMarker:
11831197
autouse: bool = False
11841198
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None
11851199
name: str | None = None
1200+
cache_result: bool = True
11861201

11871202
_ispytest: dataclasses.InitVar[bool] = False
11881203

@@ -1225,6 +1240,7 @@ def fixture(
12251240
autouse: bool = ...,
12261241
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
12271242
name: str | None = ...,
1243+
cache_result: bool = True,
12281244
) -> FixtureFunction: ...
12291245

12301246

@@ -1237,6 +1253,7 @@ def fixture(
12371253
autouse: bool = ...,
12381254
ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
12391255
name: str | None = None,
1256+
cache_result: bool = True,
12401257
) -> FixtureFunctionMarker: ...
12411258

12421259

@@ -1248,6 +1265,7 @@ def fixture(
12481265
autouse: bool = False,
12491266
ids: Sequence[object | None] | Callable[[Any], object | None] | None = None,
12501267
name: str | None = None,
1268+
cache_result: bool = True,
12511269
) -> FixtureFunctionMarker | FixtureFunction:
12521270
"""Decorator to mark a fixture factory function.
12531271
@@ -1298,6 +1316,11 @@ def fixture(
12981316
function arg that requests the fixture; one way to resolve this is to
12991317
name the decorated function ``fixture_<fixturename>`` and then use
13001318
``@pytest.fixture(name='<fixturename>')``.
1319+
1320+
:param cache_result:
1321+
If True (the default), the fixture result is cached and the fixture
1322+
only runs once per scope.
1323+
If False, the fixture will run each time it is requested
13011324
"""
13021325
fixture_marker = FixtureFunctionMarker(
13031326
scope=scope,
@@ -1306,6 +1329,7 @@ def fixture(
13061329
ids=None if ids is None else ids if callable(ids) else tuple(ids),
13071330
name=name,
13081331
_ispytest=True,
1332+
cache_result=cache_result
13091333
)
13101334

13111335
# Direct decoration.
@@ -1636,6 +1660,7 @@ def _register_fixture(
16361660
params: Sequence[object] | None = None,
16371661
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
16381662
autouse: bool = False,
1663+
cache_result: bool = True,
16391664
) -> None:
16401665
"""Register a fixture
16411666
@@ -1666,6 +1691,7 @@ def _register_fixture(
16661691
params=params,
16671692
ids=ids,
16681693
_ispytest=True,
1694+
use_cache=cache_result,
16691695
)
16701696

16711697
faclist = self._arg2fixturedefs.setdefault(name, [])
@@ -1762,6 +1788,7 @@ def parsefactories(
17621788
params=marker.params,
17631789
ids=marker.ids,
17641790
autouse=marker.autouse,
1791+
cache_result=marker.cache_result
17651792
)
17661793

17671794
def getfixturedefs(

testing/test_no_cache.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from _pytest.pytester import Pytester
2+
3+
4+
def test_setup_teardown_executed_for_every_fixture_usage_without_caching(pytester: Pytester) -> None:
5+
pytester.makepyfile(
6+
"""
7+
import pytest
8+
import logging
9+
10+
@pytest.fixture(cache_result=False)
11+
def fixt():
12+
logging.info("&&Setting up fixt&&")
13+
yield
14+
logging.info("&&Tearing down fixt&&")
15+
16+
17+
@pytest.fixture()
18+
def a(fixt):
19+
...
20+
21+
22+
@pytest.fixture()
23+
def b(fixt):
24+
...
25+
26+
27+
def test(a, b, fixt):
28+
assert False
29+
""")
30+
31+
result = pytester.runpytest("--log-level=INFO")
32+
assert result.ret == 1
33+
result.stdout.fnmatch_lines([
34+
*["*&&Setting up fixt&&*"] * 3,
35+
*["*&&Tearing down fixt&&*"] * 3,
36+
])
37+
38+
39+
def test_setup_teardown_executed_for_every_getfixturevalue_usage_without_caching(pytester: Pytester) -> None:
40+
pytester.makepyfile(
41+
"""
42+
import pytest
43+
import logging
44+
45+
@pytest.fixture(cache_result=False)
46+
def fixt():
47+
logging.info("&&Setting up fixt&&")
48+
yield
49+
logging.info("&&Tearing down fixt&&")
50+
51+
52+
def test(request):
53+
random_nums = [request.getfixturevalue('fixt') for _ in range(3)]
54+
assert False
55+
"""
56+
)
57+
result = pytester.runpytest("--log-level=INFO")
58+
assert result.ret == 1
59+
result.stdout.fnmatch_lines([
60+
*["*&&Setting up fixt&&*"] * 3,
61+
*["*&&Tearing down fixt&&*"] * 3,
62+
])
63+
64+
65+
def test_non_cached_fixture_generates_unique_values_per_usage(pytester: Pytester) -> None:
66+
pytester.makepyfile(
67+
"""
68+
import pytest
69+
70+
@pytest.fixture(cache_result=False)
71+
def random_num():
72+
import random
73+
return random.randint(-100_000_000_000, 100_000_000_000)
74+
75+
76+
@pytest.fixture()
77+
def a(random_num):
78+
return random_num
79+
80+
81+
@pytest.fixture()
82+
def b(random_num):
83+
return random_num
84+
85+
86+
def test(a, b, random_num):
87+
assert a != b != random_num
88+
""")
89+
pytester.runpytest().assert_outcomes(passed=1)
90+
91+
92+
def test_non_cached_fixture_generates_unique_values_per_getfixturevalue_usage(pytester: Pytester) -> None:
93+
pytester.makepyfile(
94+
"""
95+
import pytest
96+
97+
@pytest.fixture(cache_result=False)
98+
def random_num():
99+
import random
100+
yield random.randint(-100_000_000_000, 100_000_000_000)
101+
102+
103+
def test(request):
104+
random_nums = [request.getfixturevalue('random_num') for _ in range(3)]
105+
assert random_nums[0] != random_nums[1] != random_nums[2]
106+
"""
107+
)
108+
pytester.runpytest().assert_outcomes(passed=1)

0 commit comments

Comments
 (0)