diff --git a/changelog/6737.breaking.rst b/changelog/6737.breaking.rst new file mode 100644 index 000000000..aad661e87 --- /dev/null +++ b/changelog/6737.breaking.rst @@ -0,0 +1,7 @@ +The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when +the result is unavailable, instead of being deleted. + +If your plugin perform checks like ``hasattr(fixturedef, 'cached_result')``, +for example in a ``pytest_fixture_post_finalizer`` hook implementation, replace +it with ``fixturedef.cached_result is not None``. If you ``del`` the attribute, +set it to ``None`` instead. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bd2abb385..cdd249d93 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -855,6 +855,7 @@ class FixtureDef: self.argnames = getfuncargnames(func, name=argname, is_method=unittest) self.unittest = unittest self.ids = ids + self.cached_result = None self._finalizers = [] def addfinalizer(self, finalizer): @@ -881,8 +882,7 @@ class FixtureDef: # the cached fixture value and remove # all finalizers because they may be bound methods which will # keep instances alive - if hasattr(self, "cached_result"): - del self.cached_result + self.cached_result = None self._finalizers = [] def execute(self, request): @@ -894,9 +894,8 @@ class FixtureDef: fixturedef.addfinalizer(functools.partial(self.finish, request=request)) my_cache_key = self.cache_key(request) - cached_result = getattr(self, "cached_result", None) - if cached_result is not None: - result, cache_key, err = cached_result + if self.cached_result is not None: + result, cache_key, err = self.cached_result # note: comparison with `==` can fail (or be expensive) for e.g. # numpy arrays (#6497) if my_cache_key is cache_key: @@ -908,7 +907,7 @@ class FixtureDef: # we have a previous but differently parametrized fixture instance # so we need to tear it down before creating a new one self.finish(request) - assert not hasattr(self, "cached_result") + assert self.cached_result is None hook = self._fixturemanager.session.gethookproxy(request.node.fspath) return hook.pytest_fixture_setup(fixturedef=self, request=request) @@ -953,6 +952,7 @@ def pytest_fixture_setup(fixturedef, request): kwargs = {} for argname in fixturedef.argnames: fixdef = request._get_active_fixturedef(argname) + assert fixdef.cached_result is not None result, arg_cache_key, exc = fixdef.cached_result request._check_scope(argname, request.scope, fixdef.scope) kwargs[argname] = result diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 3cd7f5ffe..62e2155a2 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -423,9 +423,9 @@ def pytest_fixture_setup(fixturedef, request): def pytest_fixture_post_finalizer(fixturedef, request): - """ called after fixture teardown, but before the cache is cleared so - the fixture result cache ``fixturedef.cached_result`` can - still be accessed.""" + """Called after fixture teardown, but before the cache is cleared, so + the fixture result ``fixturedef.cached_result`` is still available (not + ``None``).""" # ------------------------------------------------------------------------- diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index a277ebc85..aa5a95ff9 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -34,8 +34,8 @@ def pytest_fixture_setup(fixturedef, request): _show_fixture_action(fixturedef, "SETUP") -def pytest_fixture_post_finalizer(fixturedef): - if hasattr(fixturedef, "cached_result"): +def pytest_fixture_post_finalizer(fixturedef) -> None: + if fixturedef.cached_result is not None: config = fixturedef._fixturemanager.config if config.option.setupshow: _show_fixture_action(fixturedef, "TEARDOWN")