Skip to content

Commit ef380d7

Browse files
Do the improvement
1 parent 0b4a557 commit ef380d7

File tree

5 files changed

+238
-81
lines changed

5 files changed

+238
-81
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ Ross Lawley
327327
Ruaridh Williamson
328328
Russel Winder
329329
Ryan Wooden
330+
Sadra Barikbin
330331
Saiprasad Kale
331332
Samuel Colvin
332333
Samuel Dion-Girardeau

src/_pytest/config/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,9 @@ def _get_legacy_hook_marks(
353353
if TYPE_CHECKING:
354354
# abuse typeguard from importlib to avoid massive method type union thats lacking a alias
355355
assert inspect.isroutine(method)
356-
known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
357-
must_warn: list[str] = []
358-
opts: dict[str, bool] = {}
356+
known_marks: Set[str] = {m.name for m in getattr(method, "pytestmark", [])}
357+
must_warn: List[str] = []
358+
opts: Dict[str, bool] = {}
359359
for opt_name in opt_names:
360360
opt_attr = getattr(method, opt_name, AttributeError)
361361
if opt_attr is not AttributeError:

src/_pytest/fixtures.py

Lines changed: 54 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -383,33 +383,6 @@ class FuncFixtureInfo:
383383
# sequence is ordered from furthest to closes to the function.
384384
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
385385

386-
def prune_dependency_tree(self) -> None:
387-
"""Recompute names_closure from initialnames and name2fixturedefs.
388-
389-
Can only reduce names_closure, which means that the new closure will
390-
always be a subset of the old one. The order is preserved.
391-
392-
This method is needed because direct parametrization may shadow some
393-
of the fixtures that were included in the originally built dependency
394-
tree. In this way the dependency tree can get pruned, and the closure
395-
of argnames may get reduced.
396-
"""
397-
closure: Set[str] = set()
398-
working_set = set(self.initialnames)
399-
while working_set:
400-
argname = working_set.pop()
401-
# Argname may be smth not included in the original names_closure,
402-
# in which case we ignore it. This currently happens with pseudo
403-
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
404-
# So they introduce the new dependency 'request' which might have
405-
# been missing in the original tree (closure).
406-
if argname not in closure and argname in self.names_closure:
407-
closure.add(argname)
408-
if argname in self.name2fixturedefs:
409-
working_set.update(self.name2fixturedefs[argname][-1].argnames)
410-
411-
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
412-
413386

414387
class FixtureRequest:
415388
"""A request for a fixture from a test or fixture function.
@@ -1404,6 +1377,26 @@ def pytest_addoption(parser: Parser) -> None:
14041377
)
14051378

14061379

1380+
def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
1381+
"""Return all direct parametrization arguments of a node, so we don't
1382+
mistake them for fixtures.
1383+
1384+
Check https://github.com/pytest-dev/pytest/issues/5036.
1385+
1386+
These things are done later as well when dealing with parametrization
1387+
so this could be improved.
1388+
"""
1389+
parametrize_argnames: List[str] = []
1390+
for marker in node.iter_markers(name="parametrize"):
1391+
if not marker.kwargs.get("indirect", False):
1392+
p_argnames, _ = ParameterSet._parse_parametrize_args(
1393+
*marker.args, **marker.kwargs
1394+
)
1395+
parametrize_argnames.extend(p_argnames)
1396+
1397+
return parametrize_argnames
1398+
1399+
14071400
class FixtureManager:
14081401
"""pytest fixture definitions and information is stored and managed
14091402
from this class.
@@ -1453,25 +1446,6 @@ def __init__(self, session: "Session") -> None:
14531446
}
14541447
session.config.pluginmanager.register(self, "funcmanage")
14551448

1456-
def _get_direct_parametrize_args(self, node: nodes.Node) -> List[str]:
1457-
"""Return all direct parametrization arguments of a node, so we don't
1458-
mistake them for fixtures.
1459-
1460-
Check https://github.com/pytest-dev/pytest/issues/5036.
1461-
1462-
These things are done later as well when dealing with parametrization
1463-
so this could be improved.
1464-
"""
1465-
parametrize_argnames: List[str] = []
1466-
for marker in node.iter_markers(name="parametrize"):
1467-
if not marker.kwargs.get("indirect", False):
1468-
p_argnames, _ = ParameterSet._parse_parametrize_args(
1469-
*marker.args, **marker.kwargs
1470-
)
1471-
parametrize_argnames.extend(p_argnames)
1472-
1473-
return parametrize_argnames
1474-
14751449
def getfixtureinfo(
14761450
self,
14771451
node: nodes.Item,
@@ -1501,11 +1475,28 @@ def getfixtureinfo(
15011475
usefixtures = tuple(
15021476
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
15031477
)
1504-
initialnames = usefixtures + argnames
1505-
initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure(
1506-
initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
1478+
initialnames = cast(
1479+
Tuple[str],
1480+
tuple(
1481+
dict.fromkeys(
1482+
tuple(self._getautousenames(node.nodeid)) + usefixtures + argnames
1483+
)
1484+
),
1485+
)
1486+
1487+
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
1488+
names_closure = self.getfixtureclosure(
1489+
node,
1490+
initialnames,
1491+
arg2fixturedefs,
1492+
ignore_args=_get_direct_parametrize_args(node),
1493+
)
1494+
return FuncFixtureInfo(
1495+
argnames,
1496+
initialnames,
1497+
names_closure,
1498+
arg2fixturedefs,
15071499
)
1508-
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
15091500

15101501
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
15111502
nodeid = None
@@ -1538,45 +1529,38 @@ def _getautousenames(self, nodeid: str) -> Iterator[str]:
15381529

15391530
def getfixtureclosure(
15401531
self,
1541-
fixturenames: Tuple[str, ...],
15421532
parentnode: nodes.Node,
1533+
initialnames: Tuple[str],
1534+
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]],
15431535
ignore_args: Sequence[str] = (),
1544-
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
1536+
) -> List[str]:
15451537
# Collect the closure of all fixtures, starting with the given
1546-
# fixturenames as the initial set. As we have to visit all
1547-
# factory definitions anyway, we also return an arg2fixturedefs
1538+
# initialnames as the initial set. As we have to visit all
1539+
# factory definitions anyway, we also populate arg2fixturedefs
15481540
# mapping so that the caller can reuse it and does not have
15491541
# to re-discover fixturedefs again for each fixturename
15501542
# (discovering matching fixtures for a given name/node is expensive).
15511543

1552-
parentid = parentnode.nodeid
1553-
fixturenames_closure = list(self._getautousenames(parentid))
1544+
fixturenames_closure = list(initialnames)
15541545

15551546
def merge(otherlist: Iterable[str]) -> None:
15561547
for arg in otherlist:
15571548
if arg not in fixturenames_closure:
15581549
fixturenames_closure.append(arg)
15591550

1560-
merge(fixturenames)
1561-
1562-
# At this point, fixturenames_closure contains what we call "initialnames",
1563-
# which is a set of fixturenames the function immediately requests. We
1564-
# need to return it as well, so save this.
1565-
initialnames = tuple(fixturenames_closure)
1566-
1567-
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
15681551
lastlen = -1
1552+
parentid = parentnode.nodeid
15691553
while lastlen != len(fixturenames_closure):
15701554
lastlen = len(fixturenames_closure)
15711555
for argname in fixturenames_closure:
15721556
if argname in ignore_args:
15731557
continue
1558+
if argname not in arg2fixturedefs:
1559+
fixturedefs = self.getfixturedefs(argname, parentid)
1560+
if fixturedefs:
1561+
arg2fixturedefs[argname] = fixturedefs
15741562
if argname in arg2fixturedefs:
1575-
continue
1576-
fixturedefs = self.getfixturedefs(argname, parentid)
1577-
if fixturedefs:
1578-
arg2fixturedefs[argname] = fixturedefs
1579-
merge(fixturedefs[-1].argnames)
1563+
merge(arg2fixturedefs[argname][-1].argnames)
15801564

15811565
def sort_by_scope(arg_name: str) -> Scope:
15821566
try:
@@ -1587,7 +1571,7 @@ def sort_by_scope(arg_name: str) -> Scope:
15871571
return fixturedefs[-1]._scope
15881572

15891573
fixturenames_closure.sort(key=sort_by_scope, reverse=True)
1590-
return initialnames, fixturenames_closure, arg2fixturedefs
1574+
return fixturenames_closure
15911575

15921576
def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
15931577
"""Generate new tests based on parametrized fixtures used by the given metafunc"""

src/_pytest/python.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from collections import Counter
1212
from collections import defaultdict
1313
from functools import partial
14+
from functools import wraps
1415
from pathlib import Path
1516
from typing import Any
1617
from typing import Callable
@@ -59,6 +60,7 @@
5960
from _pytest.deprecated import check_ispytest
6061
from _pytest.deprecated import INSTANCE_COLLECTOR
6162
from _pytest.deprecated import NOSE_SUPPORT_METHOD
63+
from _pytest.fixtures import _get_direct_parametrize_args
6264
from _pytest.fixtures import FuncFixtureInfo
6365
from _pytest.main import Session
6466
from _pytest.mark import MARK_GEN
@@ -380,6 +382,23 @@ class _EmptyClass: pass # noqa: E701
380382
# fmt: on
381383

382384

385+
def unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree(metafunc):
386+
metafunc.parametrize = metafunc._parametrize
387+
del metafunc._parametrize
388+
if metafunc.has_dynamic_parametrize:
389+
# Dynamic direct parametrization may have shadowed some fixtures
390+
# so make sure we update what the function really needs.
391+
definition = metafunc.definition
392+
fixture_closure = definition.parent.session._fixturemanager.getfixtureclosure(
393+
definition,
394+
definition._fixtureinfo.initialnames,
395+
definition._fixtureinfo.name2fixturedefs,
396+
ignore_args=_get_direct_parametrize_args(definition) + ["request"],
397+
)
398+
definition._fixtureinfo.names_closure[:] = fixture_closure
399+
del metafunc.has_dynamic_parametrize
400+
401+
383402
class PyCollector(PyobjMixin, nodes.Collector):
384403
def funcnamefilter(self, name: str) -> bool:
385404
return self._matches_prefix_or_glob_option("python_functions", name)
@@ -476,8 +495,6 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
476495
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
477496
fixtureinfo = definition._fixtureinfo
478497

479-
# pytest_generate_tests impls call metafunc.parametrize() which fills
480-
# metafunc._calls, the outcome of the hook.
481498
metafunc = Metafunc(
482499
definition=definition,
483500
fixtureinfo=fixtureinfo,
@@ -486,11 +503,24 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
486503
module=module,
487504
_ispytest=True,
488505
)
489-
methods = []
506+
methods = [unwrap_metafunc_parametrize_and_possibly_prune_dependency_tree]
490507
if hasattr(module, "pytest_generate_tests"):
491508
methods.append(module.pytest_generate_tests)
492509
if cls is not None and hasattr(cls, "pytest_generate_tests"):
493510
methods.append(cls().pytest_generate_tests)
511+
512+
setattr(metafunc, "has_dynamic_parametrize", False)
513+
514+
@wraps(metafunc.parametrize)
515+
def set_has_dynamic_parametrize(*args, **kwargs):
516+
setattr(metafunc, "has_dynamic_parametrize", True)
517+
metafunc._parametrize(*args, **kwargs) # type: ignore[attr-defined]
518+
519+
setattr(metafunc, "_parametrize", metafunc.parametrize)
520+
setattr(metafunc, "parametrize", set_has_dynamic_parametrize)
521+
522+
# pytest_generate_tests impls call metafunc.parametrize() which fills
523+
# metafunc._calls, the outcome of the hook.
494524
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
495525

496526
if not metafunc._calls:
@@ -500,11 +530,6 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
500530
fm = self.session._fixturemanager
501531
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
502532

503-
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
504-
# with direct parametrization, so make sure we update what the
505-
# function really needs.
506-
fixtureinfo.prune_dependency_tree()
507-
508533
for callspec in metafunc._calls:
509534
subname = f"{name}[{callspec.id}]"
510535
yield Function.from_parent(

0 commit comments

Comments
 (0)