Skip to content

Commit b9e7493

Browse files
feat(fixtures-per-test): exclude pseudo fixtures from output
Addresses issue pytest-dev#11295 by excluding from the --fixtures-per-test output any 'pseudo fixture' that results from directly parametrizating a test with ``@pytest.mark.parametrize``. The justification for removing these fixtures from the report is that a) They are unintuitive. Their appearance in the fixtures-per-test report confuses new users because the fixtures created via ``@pytest.mark.parametrize`` do not confrom to the expectations established in the documentation; namely, that fixtures are - richly reusable - provide setup/teardown features - created via the ``@pytest.fixture` decorator b) They are an internal implementation detail. It is not the explicit goal of the direct parametrization mark to create a fixture; instead, pytest's internals leverages the fixture system to achieve the explicit goal: a succinct batch execution syntax. Consequently, exposing the fixtures that implement the batch execution behaviour reveal more about pytest's internals than they do about the user's own design choices and test dependencies.
1 parent 2fdee7b commit b9e7493

File tree

1 file changed

+57
-19
lines changed

1 file changed

+57
-19
lines changed

src/_pytest/python.py

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,48 @@ def _pretty_fixture_path(invocation_dir: Path, func) -> str:
15341534
return bestrelpath(invocation_dir, loc)
15351535

15361536

1537+
def _get_fixtures_per_test(test: nodes.Item) -> Optional[List[FixtureDef[object]]]:
1538+
"""Returns all fixtures used by the test item except for a) those
1539+
created by direct parametrization with ``@pytest.mark.parametrize`` and
1540+
b) those accessed dynamically with ``request.getfixturevalue``.
1541+
1542+
The justification for excluding fixtures created by direct
1543+
parametrization is that their appearance in a report would surprise
1544+
users currently learning about fixtures, as they do not conform to the
1545+
documented characteristics of fixtures (reusable, providing
1546+
setup/teardown features, and created via the ``@pytest.fixture``
1547+
decorator).
1548+
1549+
In other words, an internal detail that leverages the fixture system
1550+
to batch execute tests should not be exposed in a report that identifies
1551+
which fixtures a user is using in their tests.
1552+
"""
1553+
# Contains information on the fixtures the test item requests
1554+
# statically, if any.
1555+
fixture_info: Optional[FuncFixtureInfo] = getattr(test, "_fixtureinfo", None)
1556+
if fixture_info is None:
1557+
# The given test item does not statically request any fixtures.
1558+
return []
1559+
1560+
# In the transitive closure of fixture names required by the item
1561+
# through autouse, function parameter or @userfixture mechanisms,
1562+
# multiple overrides may have occured; for this reason, the fixture
1563+
# names are matched to a sequence of FixtureDefs.
1564+
name2fixturedefs = fixture_info.name2fixturedefs
1565+
fixturedefs = [
1566+
# The final override, which is the one the test item will utilise,
1567+
# is stored in the final position of the sequence; therefore, we
1568+
# take the FixtureDef of the final override and add it to the list.
1569+
#
1570+
# If there wasn't an ovveride, the final item will simply be the
1571+
# first item, as required.
1572+
fixturedefs[-1]
1573+
for argname, fixturedefs in sorted(name2fixturedefs.items())
1574+
if argname not in _get_direct_parametrize_args(test)
1575+
]
1576+
return fixturedefs
1577+
1578+
15371579
def show_fixtures_per_test(config):
15381580
from _pytest.main import wrap_session
15391581

@@ -1552,7 +1594,21 @@ def get_best_relpath(func) -> str:
15521594
loc = getlocation(func, invocation_dir)
15531595
return bestrelpath(invocation_dir, Path(loc))
15541596

1555-
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
1597+
def write_item(item: nodes.Item) -> None:
1598+
fixturedefs = _get_fixtures_per_test(item)
1599+
if not fixturedefs:
1600+
# This test item does not use any fixtures.
1601+
# Do not write anything.
1602+
return
1603+
1604+
tw.line()
1605+
tw.sep("-", f"fixtures used by {item.name}")
1606+
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
1607+
1608+
for fixturedef in fixturedefs:
1609+
write_fixture(fixturedef)
1610+
1611+
def write_fixture(fixture_def: FixtureDef[object]) -> None:
15561612
argname = fixture_def.argname
15571613
if verbose <= 0 and argname.startswith("_"):
15581614
return
@@ -1568,24 +1624,6 @@ def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
15681624
else:
15691625
tw.line(" no docstring available", red=True)
15701626

1571-
def write_item(item: nodes.Item) -> None:
1572-
# Not all items have _fixtureinfo attribute.
1573-
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
1574-
if info is None or not info.name2fixturedefs:
1575-
# This test item does not use any fixtures.
1576-
return
1577-
tw.line()
1578-
tw.sep("-", f"fixtures used by {item.name}")
1579-
# TODO: Fix this type ignore.
1580-
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
1581-
# dict key not used in loop but needed for sorting.
1582-
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
1583-
assert fixturedefs is not None
1584-
if not fixturedefs:
1585-
continue
1586-
# Last item is expected to be the one used by the test item.
1587-
write_fixture(fixturedefs[-1])
1588-
15891627
for session_item in session.items:
15901628
write_item(session_item)
15911629

0 commit comments

Comments
 (0)