diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 46c89dd41..a696848f0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -72,7 +72,6 @@ if TYPE_CHECKING: from _pytest.scope import _ScopeName from _pytest.main import Session - from _pytest.python import CallSpec2 from _pytest.python import Metafunc @@ -152,7 +151,7 @@ def resolve_unique_values_and_their_indices_in_parametersets( 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. + 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: :: @@ -161,7 +160,7 @@ def resolve_unique_values_and_their_indices_in_parametersets( 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. @@ -175,7 +174,9 @@ def resolve_unique_values_and_their_indices_in_parametersets( Tuple of unique parameter values and their indices in parametersets. """ indices = [] - argname_value_indices_for_hashable_ones: Dict[str, Dict[object, int]] = defaultdict(dict) + 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): @@ -183,13 +184,17 @@ def resolve_unique_values_and_their_indices_in_parametersets( 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( + 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 + except TypeError: # `value` is not hashable argname_indices.append(argvalues_count[argname]) argvalues_count[argname] += 1 unique_values[argname].append(value) @@ -222,7 +227,7 @@ class FixtureArgKey: def get_fixture_arg_key(item: nodes.Item, argname: str, scope: Scope) -> FixtureArgKey: param_index = None param_value = None - if hasattr(item, 'callspec') and argname in item.callspec.params: + if hasattr(item, "callspec") and argname in item.callspec.params: # Fixture is parametrized. if isinstance(item.callspec.params[argname], Hashable): param_value = item.callspec.params[argname] @@ -242,21 +247,21 @@ def get_fixture_arg_key(item: nodes.Item, argname: str, scope: Scope) -> Fixture item_cls = item.cls # type: ignore[attr-defined] else: item_cls = None - + return FixtureArgKey(argname, param_index, param_value, scoped_item_path, item_cls) - + def get_fixture_keys(item: nodes.Item, scope: Scope) -> Iterator[FixtureArgKey]: """Return list of keys for all parametrized arguments which match the specified scope.""" assert scope is not Scope.Function - if hasattr(item, '_fixtureinfo'): + if hasattr(item, "_fixtureinfo"): # sort this so that different calls to # get_fixture_keys will be deterministic. for argname, fixture_def in sorted(item._fixtureinfo.name2fixturedefs.items()): # In the case item is parametrized on the `argname` with # a scope, it overrides that of the fixture. - if hasattr(item, 'callspec') and argname in item.callspec._arg2scope: + if hasattr(item, "callspec") and argname in item.callspec._arg2scope: if item.callspec._arg2scope[argname] != scope: continue elif fixture_def[-1]._scope != scope: @@ -291,13 +296,19 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: for scope in reversed(HIGH_SCOPES): for key in items_by_argkey[scope]: last_item_dependent_on_key = items_by_argkey[scope][key].pop() - fixturedef = last_item_dependent_on_key._fixtureinfo.name2fixturedefs[key.argname][-1] + fixturedef = last_item_dependent_on_key._fixtureinfo.name2fixturedefs[ + key.argname + ][-1] if fixturedef.is_pseudo: continue last_item_dependent_on_key.teardown = functools.partial( - lambda other_finalizers, new_finalizer: [finalizer() for finalizer in (new_finalizer, other_finalizers)], + lambda other_finalizers, new_finalizer: [ + finalizer() for finalizer in (new_finalizer, other_finalizers) + ], last_item_dependent_on_key.teardown, - functools.partial(fixturedef.finish, last_item_dependent_on_key._request) + functools.partial( + fixturedef.finish, last_item_dependent_on_key._request + ), ) return reordered_items @@ -307,7 +318,7 @@ def fix_cache_order( 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 + current_scope: Scope, ) -> None: for scope in HIGH_SCOPES: if current_scope < scope: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3b1351b9a..feeecac24 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -38,7 +38,6 @@ from _pytest._code.code import TerminalRepr 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 final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func @@ -59,12 +58,12 @@ from _pytest.deprecated import check_ispytest from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.deprecated import NOSE_SUPPORT_METHOD -from _pytest.fixtures import (FixtureDef, - FixtureRequest, - FuncFixtureInfo, - get_scope_node, - name2pseudofixturedef_key, - resolve_unique_values_and_their_indices_in_parametersets,) +from _pytest.fixtures import FixtureDef +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 @@ -81,7 +80,6 @@ from _pytest.pathlib import ImportPathMismatchError 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 @@ -505,7 +503,6 @@ class PyCollector(PyobjMixin, nodes.Collector): if not metafunc._calls: yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) else: - # Direct parametrization may have shadowed some fixtures # so make sure we update what the function really needs. fixtureinfo.prune_dependency_tree() @@ -1336,7 +1333,12 @@ class Metafunc: ids = self._resolve_parameter_set_ids( argnames, ids, parametersets, nodeid=self.definition.nodeid ) - params_values, param_indices_list = resolve_unique_values_and_their_indices_in_parametersets(argnames, parametersets) + ( + params_values, + param_indices_list, + ) = resolve_unique_values_and_their_indices_in_parametersets( + argnames, parametersets + ) # Store used (possibly generated) ids with parametrize Marks. if _param_mark and _param_mark._param_ids_from and generated_ids is None: object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids) @@ -1377,19 +1379,20 @@ class Metafunc: params=params_values[argname], unittest=False, ids=None, - is_pseudo=True + is_pseudo=True, ) arg2fixturedefs[argname] = [fixturedef] if name2pseudofixturedef is not None: name2pseudofixturedef[argname] = fixturedef - # Create the new calls: if we are parametrize() multiple times (by applying the decorator # more than once) then we accumulate those calls generating the cartesian product # of all calls. newcalls = [] for callspec in self._calls or [CallSpec2()]: - for param_id, param_set, param_indices in zip(ids, parametersets, param_indices_list): + for param_id, param_set, param_indices in zip( + ids, parametersets, param_indices_list + ): newcallspec = callspec.setmulti( argnames=argnames, valset=param_set.values, diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 9bce04bf9..9828df6fc 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4475,7 +4475,9 @@ def test_yield_fixture_with_no_value(pytester: Pytester) -> None: assert result.ret == ExitCode.TESTS_FAILED -def test_teardown_high_scope_fixture_at_last_dependent_item_simple(pytester: Pytester) -> None: +def test_teardown_high_scope_fixture_at_last_dependent_item_simple( + pytester: Pytester, +) -> None: pytester.makepyfile( """ import pytest @@ -4486,24 +4488,28 @@ def test_teardown_high_scope_fixture_at_last_dependent_item_simple(pytester: Pyt def test_0(fixture): pass - + def test_1(fixture): print("Running test_1!") - + def test_2(): print("Running test_2!") """ ) result = pytester.runpytest("-s") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*Running test_1!*", - "*Tearing down fixture!*", - "*Running test_2!*", - ]) + result.stdout.fnmatch_lines( + [ + "*Running test_1!*", + "*Tearing down fixture!*", + "*Running test_2!*", + ] + ) -def test_teardown_high_scope_fixture_at_last_dependent_item_simple_2(pytester: Pytester) -> None: +def test_teardown_high_scope_fixture_at_last_dependent_item_simple_2( + pytester: Pytester, +) -> None: pytester.makepyfile( """ import pytest @@ -4511,7 +4517,7 @@ def test_teardown_high_scope_fixture_at_last_dependent_item_simple_2(pytester: P def fixture1(): yield print("Tearing down fixture!") - + @pytest.fixture(scope='module', params=[None]) def fixture2(): yield @@ -4519,36 +4525,42 @@ def test_teardown_high_scope_fixture_at_last_dependent_item_simple_2(pytester: P def test_0(fixture1): pass - + def test_1(fixture1, fixture2): print("Running test_1!") - + def test_2(): print("Running test_2!") """ ) result = pytester.runpytest("-s") assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*Running test_1!*", - "*Tearing down fixture!*", - "*Tearing down fixture!*", - "*Running test_2!*", - ]) + result.stdout.fnmatch_lines( + [ + "*Running test_1!*", + "*Tearing down fixture!*", + "*Tearing down fixture!*", + "*Running test_2!*", + ] + ) -def test_teardown_high_scope_fixture_at_last_dependent_item_complex(pytester: Pytester) -> None: +def test_teardown_high_scope_fixture_at_last_dependent_item_complex( + pytester: Pytester, +) -> None: pytester.makepyfile( **{ "tests/conftest.py": "import pytest\n" + "\n".join( [ - textwrap.dedent(f""" + textwrap.dedent( + f""" @pytest.fixture(scope='{scope.value}', params=[None]) def {scope.value}_scope_fixture(request): yield None print("Tearing down {scope.value}_scope_fixture") - """) + """ + ) for scope in HIGH_SCOPES ] ), @@ -4643,10 +4655,10 @@ def test_reorder_with_nonparametrized_fixtures(pytester: Pytester): def test_2(a): pass - + def test_3(b): pass - + def test_4(b): pass """ @@ -4655,7 +4667,9 @@ def test_reorder_with_nonparametrized_fixtures(pytester: Pytester): result.stdout.fnmatch_lines([f"*test_{i}*" for i in [0, 2, 1, 3, 4]]) -def test_reorder_with_both_parametrized_and_nonparametrized_fixtures(pytester: Pytester): +def test_reorder_with_both_parametrized_and_nonparametrized_fixtures( + pytester: Pytester, +): path = pytester.makepyfile( """ import pytest @@ -4699,24 +4713,25 @@ def test_add_new_test_dependent_on_a_fixuture_and_use_nfplugin(pytester: Pyteste """ path = pytester.makepyfile(test_module_string) result = pytester.runpytest(path, "-s") - result.stdout.fnmatch_lines([ - "*Tearing down fixture!*", - "*Running test_1!*" - ]) + result.stdout.fnmatch_lines(["*Tearing down fixture!*", "*Running test_1!*"]) test_module_string += """ def test_2(fixture): pass """ path = pytester.makepyfile(test_module_string) result = pytester.runpytest(path, "--new-first", "-s") - result.stdout.fnmatch_lines([ - "*Tearing down fixture!*", - "*Running test_1!*", - "*Tearing down fixture!*", - ]) + result.stdout.fnmatch_lines( + [ + "*Tearing down fixture!*", + "*Running test_1!*", + "*Tearing down fixture!*", + ] + ) -def test_last_dependent_test_on_a_fixture_is_in_last_failed_using_lfplugin(pytester: Pytester): +def test_last_dependent_test_on_a_fixture_is_in_last_failed_using_lfplugin( + pytester: Pytester, +): test_module_string = """ import pytest @@ -4732,7 +4747,7 @@ def test_last_dependent_test_on_a_fixture_is_in_last_failed_using_lfplugin(pytes def test_1(fixture): print("Running test_1!") assert True - + def test_2(): print("Running test_2!") assert {0} @@ -4741,29 +4756,35 @@ def test_last_dependent_test_on_a_fixture_is_in_last_failed_using_lfplugin(pytes result = pytester.runpytest(path) path = pytester.makepyfile(test_module_string.format("True")) result = pytester.runpytest(path, "--last-failed", "-s") - result.stdout.fnmatch_lines([ - "*Running test_0!*", - "*Running test_2!*", - "*Tearing down fixture!*", - ]) + result.stdout.fnmatch_lines( + [ + "*Running test_0!*", + "*Running test_2!*", + "*Tearing down fixture!*", + ] + ) -@pytest.mark.xfail(reason="We do not attempt to tear down early the fixture that is overridden and also is used") -def test_early_teardown_of_overridden_and_being_used_fixture(pytester: Pytester) -> None: - pytester.makeconftest( - """ +@pytest.mark.xfail( + reason="We do not attempt to tear down early the fixture that is overridden and also is used" +) +def test_early_teardown_of_overridden_and_being_used_fixture( + pytester: Pytester, +) -> None: + pytester.makeconftest( + """ import pytest - + @pytest.fixture(scope='module') def fixture0(): yield None print("Tearing down higher-level fixture0") """ - ) - pytester.makepyfile( - """ + ) + pytester.makepyfile( + """ import pytest - + @pytest.fixture(scope='module') def fixture0(fixture0): yield None @@ -4771,20 +4792,24 @@ def test_early_teardown_of_overridden_and_being_used_fixture(pytester: Pytester) def test_0(fixture0): pass - + def test_1(): print("Both `fixture0`s should have been torn down") """ - ) - result = pytester.runpytest("-s") - result.stdout.fnmatch_lines([ + ) + result = pytester.runpytest("-s") + result.stdout.fnmatch_lines( + [ "*Tearing down lower-level fixture0*", "*Tearing down higher-level fixture0*", "*Both `fixture0`s should have been torn down*", - ]) + ] + ) -def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices(pytester: Pytester): +def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices( + pytester: Pytester, +): pytester.makepyfile( """ import pytest @@ -4804,39 +4829,46 @@ def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices(pyt def test_2(): print("fixture1 should have been torn down 3 times") - + @pytest.mark.parametrize("param", [0,1,2], scope='module') def test_3(param): pass - + @pytest.mark.parametrize("param", [2,1,0], scope='module') def test_4(param): pass - """) + """ + ) result = pytester.runpytest("--collect-only") - result.stdout.re_match_lines([ - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - ]) + result.stdout.re_match_lines( + [ + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + ] + ) result = pytester.runpytest("-s") - result.stdout.fnmatch_lines([ - "*Tearing down fixture1 with param value `1`*", - "*Tearing down fixture1 with param value `0`*", - "*Tearing down fixture1 with param value `2`*", - "*fixture1 should have been torn down 3 times*", - ]) + result.stdout.fnmatch_lines( + [ + "*Tearing down fixture1 with param value `1`*", + "*Tearing down fixture1 with param value `0`*", + "*Tearing down fixture1 with param value `2`*", + "*fixture1 should have been torn down 3 times*", + ] + ) -def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices_2(pytester: Pytester): +def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices_2( + pytester: Pytester, +): pytester.makepyfile( """ import pytest @@ -4858,47 +4890,54 @@ def test_basing_fixture_argkeys_on_param_values_rather_than_on_param_indices_2(p @pytest.mark.parametrize("fixture1, fixture2", [("c", 4), ("a", 3)], indirect=True) def test_2(fixture1, fixture2): pass - + def test_3(): print("All fixtures should have been torn down") - + @pytest.mark.parametrize("param1, param2", [("a", 0), ("b", 1), ("a", 2)], scope='module') def test_4(param1, param2): pass - + @pytest.mark.parametrize("param1, param2", [("c", 4), ("a", 3)], scope='module') def test_5(param1, param2): pass - """) + """ + ) result = pytester.runpytest("--collect-only") - result.stdout.re_match_lines([ - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - r" ", - ]) + result.stdout.re_match_lines( + [ + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + r" ", + ] + ) result = pytester.runpytest("-s") - result.stdout.fnmatch_lines([ - "*Tearing down fixture2 with param value `0`*", - "*Tearing down fixture2 with param value `2`*", - "*Tearing down fixture2 with param value `3`*", - "*Tearing down fixture1 with param value `a`*", - "*Tearing down fixture2 with param value `1`*", - "*Tearing down fixture1 with param value `b`*", - "*Tearing down fixture2 with param value `4`*", - "*Tearing down fixture1 with param value `c`*", - "*All fixtures should have been torn down*", - ]) + result.stdout.fnmatch_lines( + [ + "*Tearing down fixture2 with param value `0`*", + "*Tearing down fixture2 with param value `2`*", + "*Tearing down fixture2 with param value `3`*", + "*Tearing down fixture1 with param value `a`*", + "*Tearing down fixture2 with param value `1`*", + "*Tearing down fixture1 with param value `b`*", + "*Tearing down fixture2 with param value `4`*", + "*Tearing down fixture1 with param value `c`*", + "*All fixtures should have been torn down*", + ] + ) -def test_early_teardown_when_an_item_is_the_last_dependent_on_multiple_fixtures(pytester: Pytester): +def test_early_teardown_when_an_item_is_the_last_dependent_on_multiple_fixtures( + pytester: Pytester, +): pytester.makepyfile( """ import pytest @@ -4907,7 +4946,7 @@ def test_early_teardown_when_an_item_is_the_last_dependent_on_multiple_fixtures( def fixture1(): yield None print("Tearing down fixture1") - + @pytest.fixture(scope='module') def fixture2(): yield None @@ -4929,21 +4968,24 @@ def test_early_teardown_when_an_item_is_the_last_dependent_on_multiple_fixtures( def test_3(fixture1, fixture2, fixture3): print("No fixture should have been torn down") - + def test_4(): print("All fixtures should have been torn down") - """) + """ + ) result = pytester.runpytest("-s") - result.stdout.fnmatch_lines([ - "*No fixture should have been torn down*", - "*No fixture should have been torn down*", - "*No fixture should have been torn down*", - "*No fixture should have been torn down*", - "*Tearing down fixture3*", - "*Tearing down fixture2*", - "*Tearing down fixture1*", - "*All fixtures should have been torn down*", - ]) + result.stdout.fnmatch_lines( + [ + "*No fixture should have been torn down*", + "*No fixture should have been torn down*", + "*No fixture should have been torn down*", + "*No fixture should have been torn down*", + "*Tearing down fixture3*", + "*Tearing down fixture2*", + "*Tearing down fixture1*", + "*All fixtures should have been torn down*", + ] + ) def test_early_teardown_does_not_occur_for_pseudo_fixtures(pytester: Pytester) -> None: @@ -4958,7 +5000,7 @@ def test_early_teardown_does_not_occur_for_pseudo_fixtures(pytester: Pytester) - @pytest.mark.parametrize("param", [0,1,2], scope='module') def test_0(param): pass - + @pytest.mark.parametrize("param", [0,1,2], scope='module') def test_1(param): pass @@ -4966,4 +5008,5 @@ def test_early_teardown_does_not_occur_for_pseudo_fixtures(pytester: Pytester) - ) items = pytester.inline_run().getcalls("pytest_collection_finish")[0].session.items import functools - assert not any([isinstance(item.teardown, functools.partial) for item in items]) \ No newline at end of file + + assert not any([isinstance(item.teardown, functools.partial) for item in items]) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 83ef30442..a1a39b87e 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -982,7 +982,7 @@ class TestMetafunc: assert metafunc._calls[1].params == dict(x=3, y=4) assert metafunc._calls[1].funcargs == {} assert metafunc._calls[1].id == "3-4" - + def test_parametrize_with_duplicate_values(self) -> None: metafunc = self.Metafunc(lambda x, y: None) metafunc.parametrize(("x", "y"), [(1, 2), (3, 4), (1, 5), (2, 2)])