diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 3d868266344..f978aa2fc89 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -65,7 +65,6 @@ from _pytest.pathlib import bestrelpath from _pytest.scope import HIGH_SCOPES from _pytest.scope import Scope -from _pytest.stash import StashKey if TYPE_CHECKING: @@ -149,66 +148,6 @@ def get_scope_node( assert_never(scope) -def resolve_unique_values_and_their_indices_in_parametersets( - argnames: Sequence[str], - parametersets: Sequence[ParameterSet], -) -> Tuple[Dict[str, List[object]], List[Tuple[int]]]: - """Resolve unique values and their indices in parameter sets. The index of a value - is determined by when it appears in the possible values for the first time. - For example, given ``argnames`` and ``parametersets`` below, the result would be: - - :: - - argnames = ["A", "B", "C"] - parametersets = [("a1", "b1", "c1"), ("a1", "b2", "c1"), ("a1", "b3", "c2")] - result[0] = {"A": ["a1"], "B": ["b1", "b2", "b3"], "C": ["c1", "c2"]} - result[1] = [(0, 0, 0), (0, 1, 0), (0, 2, 1)] - - result is used in reordering `indirect`ly parametrized with multiple - parameters or directly parametrized tests to keep items using the same fixture or - pseudo-fixture values respectively, close together. - - :param argnames: - Argument names passed to ``parametrize()``. - :param parametersets: - The parameter sets, each containing a set of values corresponding - to ``argnames``. - :returns: - Tuple of unique parameter values and their indices in parametersets. - """ - indices = [] - argname_value_indices_for_hashable_ones: Dict[str, Dict[object, int]] = defaultdict( - dict - ) - argvalues_count: Dict[str, int] = defaultdict(lambda: 0) - unique_values: Dict[str, List[object]] = defaultdict(list) - for i, argname in enumerate(argnames): - argname_indices = [] - for parameterset in parametersets: - value = parameterset.values[i] - try: - argname_indices.append( - argname_value_indices_for_hashable_ones[argname][value] - ) - except KeyError: # New unique value - argname_value_indices_for_hashable_ones[argname][ - value - ] = argvalues_count[argname] - argname_indices.append(argvalues_count[argname]) - argvalues_count[argname] += 1 - unique_values[argname].append(value) - except TypeError: # `value` is not hashable - argname_indices.append(argvalues_count[argname]) - argvalues_count[argname] += 1 - unique_values[argname].append(value) - indices.append(argname_indices) - return unique_values, list(zip(*indices)) - - -# Used for storing artificial fixturedefs for direct parametrization. -name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]() - - def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: """Return fixturemarker or None if it doesn't exist or raised exceptions.""" @@ -352,15 +291,9 @@ def fix_cache_order( item: nodes.Item, argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]], items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]], - ignore: Set[Optional[FixtureArgKey]], - current_scope: Scope, ) -> None: for scope in HIGH_SCOPES: - if current_scope < scope: - continue for key in argkeys_cache[scope].get(item, []): - if key in ignore: - continue items_by_argkey[scope][key].appendleft(item) @@ -404,11 +337,17 @@ def reorder_items_atscope( else: slicing_argkey, _ = argkeys.popitem() # deque because they'll just be ignored. - unique_matching_items = dict.fromkeys(scoped_items_by_argkey[slicing_argkey]) - for i in reversed(unique_matching_items if sys.version_info.minor > 7 else list(unique_matching_items)): + unique_matching_items = dict.fromkeys( + scoped_items_by_argkey[slicing_argkey] + ) + for i in reversed( + unique_matching_items + if sys.version_info.minor > 7 + else list(unique_matching_items) + ): if i not in items: continue - fix_cache_order(i, argkeys_cache, items_by_argkey, ignore, scope) + fix_cache_order(i, argkeys_cache, items_by_argkey) items_deque.appendleft(i) break if no_argkey_group: @@ -447,18 +386,6 @@ class FuncFixtureInfo: name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]] name2num_fixturedefs_used: Dict[str, int] - def prune_dependency_tree(self) -> None: - """Recompute names_closure from initialnames and name2fixturedefs. - - Can only reduce names_closure, which means that the new closure will - always be a subset of the old one. The order is preserved. - - This method is needed because dynamic direct parametrization may shadow - some of the fixtures that were included in the originally built dependency - tree. In this way the dependency tree can get pruned, and the closure - of argnames may get reduced. - """ - class FixtureRequest: """A request for a fixture from a test or fixture function. @@ -1585,8 +1512,8 @@ def getfixtureclosure( ignore_args: Sequence[str] = (), ) -> Tuple[List[str], Dict[str, List[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given - # fixturenames as the initial set. As we have to visit all - # factory definitions anyway, we also return an arg2fixturedefs + # initialnames as the initial set. As we have to visit all + # factory definitions anyway, we also populate arg2fixturedefs # mapping so that the caller can reuse it and does not have # to re-discover fixturedefs again for each fixturename # (discovering matching fixtures for a given name/node is expensive). @@ -1594,10 +1521,6 @@ def getfixtureclosure( parentid = parentnode.nodeid fixturenames_closure: Dict[str, int] = {} - # At this point, fixturenames_closure contains what we call "initialnames", - # which is a set of fixturenames the function immediately requests. We - # need to return it as well, so save this. - arg2num_fixturedefs_used: Dict[str, int] = defaultdict(lambda: 0) arg2num_def_used_in_path: Dict[str, int] = defaultdict(lambda: 0) nodes_in_fixture_tree: Deque[Tuple[str, bool]] = deque( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1d167cfda91..2bcc923f180 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -40,7 +40,6 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped -from _pytest.compat import assert_never from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func from _pytest.compat import getimfunc @@ -65,8 +64,6 @@ from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import get_scope_node -from _pytest.fixtures import name2pseudofixturedef_key -from _pytest.fixtures import resolve_unique_values_and_their_indices_in_parametersets from _pytest.main import Session from _pytest.mark import MARK_GEN from _pytest.mark import ParameterSet @@ -83,6 +80,7 @@ from _pytest.pathlib import parts from _pytest.pathlib import visit from _pytest.scope import Scope +from _pytest.stash import StashKey from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning @@ -1151,11 +1149,8 @@ class CallSpec2: and stored in item.callspec. """ - # arg name -> arg value which will be passed to the parametrized test - # function (direct parameterization). - funcargs: Dict[str, object] = dataclasses.field(default_factory=dict) - # arg name -> arg value which will be passed to a fixture of the same name - # (indirect parametrization). + # arg name -> arg value which will be passed to a fixture or pseudo-fixture + # of the same name. (indirect or direct parametrization respectively) params: Dict[str, object] = dataclasses.field(default_factory=dict) # arg name -> arg index. indices: Dict[str, int] = dataclasses.field(default_factory=dict) @@ -1208,6 +1203,65 @@ def get_direct_param_fixture_func(request: FixtureRequest) -> Any: return request.param +def resolve_unique_values_and_their_indices_in_parametersets( + argnames: Sequence[str], + parametersets: Sequence[ParameterSet], +) -> Tuple[Dict[str, List[object]], List[Tuple[int]]]: + """Resolve unique values and represent parameter sets by values' indices. The index of + a value in a parameter set is determined by where the value appears in the existing values + of the argname for the first time. For example, given ``argnames`` and ``parametersets`` + below, the result would be: + + :: + + argnames = ["A", "B", "C"] + parametersets = [("a1", "b1", "c1"), ("a1", "b2", "c1"), ("a1", "b3", "c2")] + result[0] = {"A": ["a1"], "B": ["b1", "b2", "b3"], "C": ["c1", "c2"]} + result[1] = [(0, 0, 0), (0, 1, 0), (0, 2, 1)] + + result is used in reordering tests to keep items using the same fixture close together. + + :param argnames: + Argument names passed to ``parametrize()``. + :param parametersets: + The parameter sets, each containing a set of values corresponding + to ``argnames``. + :returns: + Tuple of unique parameter values and their indices in parametersets. + """ + indices = [] + argname_value_indices_for_hashable_ones: Dict[str, Dict[object, int]] = defaultdict( + dict + ) + argvalues_count: Dict[str, int] = defaultdict(lambda: 0) + unique_values: Dict[str, List[object]] = defaultdict(list) + for i, argname in enumerate(argnames): + argname_indices = [] + for parameterset in parametersets: + value = parameterset.values[i] + try: + argname_indices.append( + argname_value_indices_for_hashable_ones[argname][value] + ) + except KeyError: # New unique value + argname_value_indices_for_hashable_ones[argname][ + value + ] = argvalues_count[argname] + argname_indices.append(argvalues_count[argname]) + argvalues_count[argname] += 1 + unique_values[argname].append(value) + except TypeError: # `value` is not hashable + argname_indices.append(argvalues_count[argname]) + argvalues_count[argname] += 1 + unique_values[argname].append(value) + indices.append(argname_indices) + return unique_values, list(zip(*indices)) + + +# Used for storing artificial fixturedefs for direct parametrization. +name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]() + + @final class Metafunc: """Objects passed to the :hook:`pytest_generate_tests` hook.