Skip to content

Commit

Permalink
Merge pull request pytest-dev#12092 from bluetech/fixture-cleanup
Browse files Browse the repository at this point in the history
fixtures: a few more cleanups
  • Loading branch information
bluetech committed Mar 9, 2024
2 parents 9033d4d + ff551b7 commit 437eb86
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 36 deletions.
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

0 comments on commit 437eb86

Please sign in to comment.