Skip to content

Move fixture cache from FixtureDef to SetupState#14770

Open
seifertm wants to merge 25 commits into
pytest-dev:mainfrom
seifertm:move_fixture_cache_from_fixturedef_to_setupstate
Open

Move fixture cache from FixtureDef to SetupState#14770
seifertm wants to merge 25 commits into
pytest-dev:mainfrom
seifertm:move_fixture_cache_from_fixturedef_to_setupstate

Conversation

@seifertm

@seifertm seifertm commented Jul 23, 2026

Copy link
Copy Markdown

This PR removes FixtureDef.cached_result in favor of a cache that is attached to SetupState. FixtureRequest received methods to manipulate the fixture cache.

The PR is easiest reviewed commit by commit.

Open issues

Previously, fixture values were cached as part of FixtureDef objects and the cache entries contained a cache_key. The cache key is either None for non-parametrized fixtures or SubRequest.param. This allows invalidating cache entries when fixture parameters change.

Hoisting up the fixture cache to SetupState technically makes FixtureDef part of the cache key. This means we should use a two-dimensional index (e.g. tuple[FixtureDef, param]) to store entries in the fixture cache. Unfortunately, this is tricky, because TopRequest doesn't know about the current fixture param and therefore doesn't know the cache key.

This work has been done as part of the pytest sprint 2026, sponsored by Omicron.

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jul 23, 2026
@seifertm
seifertm force-pushed the move_fixture_cache_from_fixturedef_to_setupstate branch from c54967f to 6678e5d Compare July 23, 2026 10:19
@seifertm
seifertm marked this pull request as ready for review July 23, 2026 10:19
Comment thread src/_pytest/setuponly.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed

@seifertm seifertm Jul 24, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find, thanks @RonnyPfannschmidt !

I pushed 5 additional commits. The tracking of the currently active request parameter for --setup-show is no longer attached to FixtureDef. Instead, --setup-show uses the existing SetupState.fixture_cache, which already contains the active fixture param as a cache key. The variable naming was changed from "cache_key" to "param" or "active_param".

In order to distinguish between "no param" and "param=None", I introduced a sentinel value. This prevents --setup-show from printing my_fixture[None] for unparametrized fixtures.

@bluetech

Copy link
Copy Markdown
Member

Previous stalled PR you might want to use as a reference/comparison: #14104

@seifertm

Copy link
Copy Markdown
Author

Thanks @bluetech! This topic was raised during the pytest sprint and I wasn't aware that there have been previous attempts at this. I'll check out the PR.

seifertm added 18 commits July 24, 2026 11:34
…hing fixture results to distinguish between *no param* and *None*.
@seifertm
seifertm force-pushed the move_fixture_cache_from_fixturedef_to_setupstate branch from cf3854d to 61286bd Compare July 24, 2026 09:34
@seifertm

Copy link
Copy Markdown
Author

Previous stalled PR you might want to use as a reference/comparison: #14104

The linked PR appears to rework fixture evaluation logic and move that to runner.py. This PR reduces the mutability of FixtureDef by moving the fixture cache from FixtureDef to SetupState. On first sight, both PRs seem to be complementary rather than competing with each other.

Comment thread src/_pytest/fixtures.py
Comment on lines +105 to +110
if TYPE_CHECKING and sys.version_info >= (3, 11):

class _FixtureResult(NamedTuple, Generic[FixtureValue]):
value: FixtureValue
param: object
exception_and_traceback: None

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is marked as uncovered or partially covered, even though it's only used for type checking.

Do you have any suggestions how I can deal with that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add a # pragma: no cover on the if line.

Or better yet, update the exceptions in

pytest/pyproject.toml

Lines 476 to 487 in ef052d0

exclude_lines = [
'\#\s*pragma: no cover',
'^\s*raise NotImplementedError\b',
'^\s*return NotImplemented\b',
'^\s*assert False(,|$)',
'^\s*case unreachable:',
'^\s*assert_never\(',
'^\s*if TYPE_CHECKING:',
'^\s*(el)?if TYPE_CHECKING:',
'^\s*@overload( |$)',
'^\s*def .+: \.\.\.$',
'^\s*@pytest\.mark\.xfail',
to if TYPE_CHECKING\s.*

Comment thread changelog/14770.misc.rst Outdated
Comment thread src/_pytest/fixtures.py
Comment on lines +105 to +110
if TYPE_CHECKING and sys.version_info >= (3, 11):

class _FixtureResult(NamedTuple, Generic[FixtureValue]):
value: FixtureValue
param: object
exception_and_traceback: None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add a # pragma: no cover on the if line.

Or better yet, update the exceptions in

pytest/pyproject.toml

Lines 476 to 487 in ef052d0

exclude_lines = [
'\#\s*pragma: no cover',
'^\s*raise NotImplementedError\b',
'^\s*return NotImplemented\b',
'^\s*assert False(,|$)',
'^\s*case unreachable:',
'^\s*assert_never\(',
'^\s*if TYPE_CHECKING:',
'^\s*(el)?if TYPE_CHECKING:',
'^\s*@overload( |$)',
'^\s*def .+: \.\.\.$',
'^\s*@pytest\.mark\.xfail',
to if TYPE_CHECKING\s.*

Comment thread src/_pytest/fixtures.py
# If the fixture was executed, the current value of the fixture.
# Can change if the fixture is executed with different parameters.
self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
self._finalizers: Final[list[Callable[[], object]]] = []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to the PR, more of a question (@RonnyPfannschmidt): what about the finalizers? We likely would also want finalizers to be hosted per-thread?

Comment thread src/_pytest/runner.py
Comment on lines +514 to +516
# Importing the appropriate types from the fixtures module lead to circular
# imports, so we leave the cache untyped for now
self.fixture_cache = {} # type: ignore[var-annotated]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having the type checking here is important, let's move the type definitions we need to a new fixtures_types module, containing only the type definitions, which allows us to import them here without cycles.

Comment thread testing/test_setuponly.py
)


def test_suppress_capturing(pytester: Pytester) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test defined here?

@seifertm seifertm Jul 24, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was added to make the CI pass. The codecov job seems to fail, if the coverage of the PR is less than 100%. Apparently this also applies to files that are touched by the PR, but didn't have full coverage. This test specifically covers the negative branch of this if statement.

@seifertm
seifertm force-pushed the move_fixture_cache_from_fixturedef_to_setupstate branch from 1c6a7d2 to 9720a75 Compare July 24, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants