Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
75 changes: 42 additions & 33 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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]"]:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 12 additions & 3 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*",
]
Expand All @@ -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:
Expand Down Expand Up @@ -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()",
]
)

Expand Down