Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make high-scope fixtures teardown at last dependent test #10771

Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Finalize changes
  • Loading branch information
sadra-barikbin committed Jul 15, 2023
commit d2ba7fd505c7e4783a1399fbac2e7d45f646c880
99 changes: 11 additions & 88 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
@@ -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,19 +1512,15 @@ 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).

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(
70 changes: 62 additions & 8 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
@@ -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.