diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 45412159e86..4b7c107521b 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -388,6 +388,14 @@ def scope(self) -> _ScopeName: """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value + @abc.abstractmethod + def _check_scope( + self, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_scope: Scope, + ) -> None: + raise NotImplementedError() + @property def fixturenames(self) -> List[str]: """Names of all active fixtures in this request.""" @@ -565,6 +573,8 @@ def _get_active_fixturedef( raise self._compute_fixture_value(fixturedef) self._fixture_defs[argname] = fixturedef + else: + self._check_scope(fixturedef, fixturedef._scope) return fixturedef def _get_fixturestack(self) -> List["FixtureDef[Any]"]: @@ -632,12 +642,12 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: ) fail(msg, pytrace=False) + # Check if a higher-level scoped fixture accesses a lower level one. + self._check_scope(fixturedef, scope) + subrequest = SubRequest( self, scope, param, param_index, fixturedef, _ispytest=True ) - - # Check if a higher-level scoped fixture accesses a lower level one. - subrequest._check_scope(argname, self._scope, scope) try: # Call the fixture function. fixturedef.execute(request=subrequest) @@ -669,6 +679,14 @@ def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None: def _scope(self) -> Scope: return Scope.Function + def _check_scope( + self, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_scope: Scope, + ) -> None: + # TopRequest always has function scope so always valid. + pass + @property def node(self): return self._pyfuncitem @@ -740,37 +758,34 @@ def node(self): def _check_scope( self, - argname: str, - invoking_scope: Scope, + requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], requested_scope: Scope, ) -> None: - if argname == "request": + if isinstance(requested_fixturedef, PseudoFixtureDef): return - if invoking_scope > requested_scope: + if self._scope > requested_scope: # Try to report something helpful. - text = "\n".join(self._factorytraceback()) + argname = requested_fixturedef.argname + fixture_stack = "\n".join( + self._format_fixturedef_line(fixturedef) + for fixturedef in self._get_fixturestack() + ) + requested_fixture = self._format_fixturedef_line(requested_fixturedef) fail( f"ScopeMismatch: You tried to access the {requested_scope.value} scoped " - f"fixture {argname} with a {invoking_scope.value} scoped request object, " - f"involved factories:\n{text}", + f"fixture {argname} with a {self._scope.value} scoped request object. " + f"Requesting fixture stack:\n{fixture_stack}\n" + f"Requested fixture:\n{requested_fixture}", pytrace=False, ) - def _factorytraceback(self) -> List[str]: - lines = [] - for fixturedef in self._get_fixturestack(): - factory = fixturedef.func - fs, lineno = getfslineno(factory) - if isinstance(fs, Path): - session: Session = self._pyfuncitem.session - p = bestrelpath(session.path, fs) - else: - p = fs - lines.append( - "%s:%d: def %s%s" - % (p, lineno + 1, factory.__name__, inspect.signature(factory)) - ) - return lines + def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str: + factory = fixturedef.func + path, lineno = getfslineno(factory) + if isinstance(path, Path): + path = bestrelpath(self._pyfuncitem.session.path, path) + signature = inspect.signature(factory) + return f"{path}:{lineno + 1}: def {factory.__name__}{signature}" def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._fixturedef.addfinalizer(finalizer) @@ -1046,9 +1061,7 @@ def execute(self, request: SubRequest) -> FixtureValue: # with their finalization. for argname in self.argnames: fixturedef = request._get_active_fixturedef(argname) - if argname != "request": - # PseudoFixtureDef is only for "request". - assert isinstance(fixturedef, FixtureDef) + if not isinstance(fixturedef, PseudoFixtureDef): fixturedef.addfinalizer(functools.partial(self.finish, request=request)) my_cache_key = self.cache_key(request) @@ -1108,11 +1121,7 @@ def pytest_fixture_setup( """Execution of fixture setup.""" kwargs = {} for argname in fixturedef.argnames: - fixdef = request._get_active_fixturedef(argname) - assert fixdef.cached_result is not None - result, arg_cache_key, exc = fixdef.cached_result - request._check_scope(argname, request._scope, fixdef._scope) - kwargs[argname] = result + kwargs[argname] = request.getfixturevalue(argname) fixturefunc = resolve_fixture_function(fixturedef, request) my_cache_key = fixturedef.cache_key(request) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6edff6ecd43..8d59b36d319 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1247,8 +1247,9 @@ def test_add(arg2): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*ScopeMismatch*involved factories*", + "*ScopeMismatch*Requesting fixture stack*", "test_receives_funcargs_scope_mismatch.py:6: def arg2(arg1)", + "Requested fixture:", "test_receives_funcargs_scope_mismatch.py:2: def arg1()", "*1 error*", ] @@ -1274,7 +1275,13 @@ def test_add(arg1, arg2): ) result = pytester.runpytest() result.stdout.fnmatch_lines( - ["*ScopeMismatch*involved factories*", "* def arg2*", "*1 error*"] + [ + "*ScopeMismatch*Requesting fixture stack*", + "* def arg2(arg1)", + "Requested fixture:", + "* def arg1()", + "*1 error*", + ], ) def test_invalid_scope(self, pytester: Pytester) -> None: @@ -2488,8 +2495,10 @@ def test_it(request, fixfunc): assert result.ret == ExitCode.TESTS_FAILED result.stdout.fnmatch_lines( [ - "*ScopeMismatch*involved factories*", + "*ScopeMismatch*Requesting fixture stack*", "test_it.py:6: def fixmod(fixfunc)", + "Requested fixture:", + "test_it.py:3: def fixfunc()", ] )