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

fixtures: inline some functions to streamline the code #12130

Merged
merged 6 commits into from
Apr 27, 2024
Merged
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
197 changes: 92 additions & 105 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import _pytest
from _pytest import nodes
from _pytest._code import getfslineno
from _pytest._code import Source
from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
Expand Down Expand Up @@ -410,41 +411,6 @@
"""Underlying collection node (depends on current request scope)."""
raise NotImplementedError()

def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None:
# We arrive here because of a dynamic call to
# getfixturevalue(argname) usage which was naturally
# not known at parsing/collection time.
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name.
if fixturedefs is None:
raise FixtureLookupError(argname, self)
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)

# A fixture may override another fixture with the same name, e.g. a
# fixture in a module can override a fixture in a conftest, a fixture in
# a class can override a fixture in the module, and so on.
# An overriding fixture can request its own name (possibly indirectly);
# in this case it gets the value of the fixture it overrides, one level
# up.
# Check how many `argname`s deep we are, and take the next one.
# `fixturedefs` is sorted from furthest to closest, so use negative
# indexing to go in reverse.
index = -1
for request in self._iter_chain():
if request.fixturename == argname:
index -= 1
# If already consumed all of the available levels, fail.
if -index > len(fixturedefs):
raise FixtureLookupError(argname, self)

return fixturedefs[index]

@property
def config(self) -> Config:
"""The pytest config object associated with this request."""
Expand Down Expand Up @@ -569,39 +535,53 @@
def _get_active_fixturedef(
self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
if argname == "request":
cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function)

# If we already finished computing a fixture by this name in this item,
# return it.
fixturedef = self._fixture_defs.get(argname)
if fixturedef is None:
try:
fixturedef = self._getnextfixturedef(argname)
except FixtureLookupError:
if argname == "request":
cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function)
raise
self._compute_fixture_value(fixturedef)
self._fixture_defs[argname] = fixturedef
else:
if fixturedef is not None:
self._check_scope(fixturedef, fixturedef._scope)
return fixturedef

def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values
return fixturedef

def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
"""Create a SubRequest based on "self" and call the execute method
of the given FixtureDef object.
# Find the appropriate fixturedef.
fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None:
# We arrive here because of a dynamic call to
# getfixturevalue(argname) which was naturally
# not known at parsing/collection time.
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name.
if fixturedefs is None:
raise FixtureLookupError(argname, self)

Check warning on line 560 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L560

Added line #L560 was not covered by tests
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)

Check warning on line 563 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L563

Added line #L563 was not covered by tests
# A fixture may override another fixture with the same name, e.g. a
# fixture in a module can override a fixture in a conftest, a fixture in
# a class can override a fixture in the module, and so on.
# An overriding fixture can request its own name (possibly indirectly);
# in this case it gets the value of the fixture it overrides, one level
# up.
# Check how many `argname`s deep we are, and take the next one.
# `fixturedefs` is sorted from furthest to closest, so use negative
# indexing to go in reverse.
index = -1
for request in self._iter_chain():
if request.fixturename == argname:
index -= 1

Check warning on line 576 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L576

Added line #L576 was not covered by tests
# If already consumed all of the available levels, fail.
if -index > len(fixturedefs):
raise FixtureLookupError(argname, self)

Check warning on line 579 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L579

Added line #L579 was not covered by tests
fixturedef = fixturedefs[index]

If the FixtureDef has cached the result it will do nothing, otherwise it will
setup and run the fixture, cache the value, and schedule a finalizer for it.
"""
# prepare a subrequest object before calling fixture function
# (latter managed by fixturedef)
argname = fixturedef.argname
funcitem = self._pyfuncitem
# Prepare a SubRequest object for calling the fixture.
try:
callspec = funcitem.callspec
callspec = self._pyfuncitem.callspec
except AttributeError:
callspec = None
if callspec is not None and argname in callspec.params:
Expand All @@ -613,48 +593,56 @@
param = NOTSET
param_index = 0
scope = fixturedef._scope

has_params = fixturedef.params is not None
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
if has_params and fixtures_not_supported:
msg = (
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
f"Node id: {funcitem.nodeid}\n"
f"Function type: {type(funcitem).__name__}"
)
fail(msg, pytrace=False)
if has_params:
frame = inspect.stack()[3]
frameinfo = inspect.getframeinfo(frame[0])
source_path = absolutepath(frameinfo.filename)
source_lineno = frameinfo.lineno
try:
source_path_str = str(
source_path.relative_to(funcitem.config.rootpath)
)
except ValueError:
source_path_str = str(source_path)
location = getlocation(fixturedef.func, funcitem.config.rootpath)
msg = (
"The requested fixture has no parameter defined for test:\n"
f" {funcitem.nodeid}\n\n"
f"Requested fixture '{fixturedef.argname}' defined in:\n"
f"{location}\n\n"
f"Requested here:\n"
f"{source_path_str}:{source_lineno}"
)
fail(msg, pytrace=False)

# Check if a higher-level scoped fixture accesses a lower level one.
self._check_fixturedef_without_param(fixturedef)
self._check_scope(fixturedef, scope)

subrequest = SubRequest(
self, scope, param, param_index, fixturedef, _ispytest=True
)

# Make sure the fixture value is cached, running it if it isn't
fixturedef.execute(request=subrequest)

self._fixture_defs[argname] = fixturedef
return fixturedef

def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None:
"""Check that this request is allowed to execute this fixturedef without
a param."""
funcitem = self._pyfuncitem
has_params = fixturedef.params is not None
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
if has_params and fixtures_not_supported:
msg = (

Check warning on line 615 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L615

Added line #L615 was not covered by tests
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
f"Node id: {funcitem.nodeid}\n"
f"Function type: {type(funcitem).__name__}"
)
fail(msg, pytrace=False)

Check warning on line 620 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L620

Added line #L620 was not covered by tests
if has_params:
frame = inspect.stack()[3]
frameinfo = inspect.getframeinfo(frame[0])
source_path = absolutepath(frameinfo.filename)
source_lineno = frameinfo.lineno
try:
source_path_str = str(source_path.relative_to(funcitem.config.rootpath))
except ValueError:
source_path_str = str(source_path)
location = getlocation(fixturedef.func, funcitem.config.rootpath)
msg = (

Check warning on line 631 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L622-L631

Added lines #L622 - L631 were not covered by tests
"The requested fixture has no parameter defined for test:\n"
f" {funcitem.nodeid}\n\n"
f"Requested fixture '{fixturedef.argname}' defined in:\n"
f"{location}\n\n"
f"Requested here:\n"
f"{source_path_str}:{source_lineno}"
)
fail(msg, pytrace=False)

Check warning on line 639 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L639

Added line #L639 was not covered by tests

def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values

Check warning on line 644 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L643-L644

Added lines #L643 - L644 were not covered by tests


@final
class TopRequest(FixtureRequest):
Expand Down Expand Up @@ -877,13 +865,6 @@
tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))


def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
fs, lineno = getfslineno(fixturefunc)
location = f"{fs}:{lineno + 1}"
source = _pytest._code.Source(fixturefunc)
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)


def call_fixture_func(
fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
) -> FixtureValue:
Expand Down Expand Up @@ -913,7 +894,13 @@
except StopIteration:
pass
else:
fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
fs, lineno = getfslineno(fixturefunc)
fail(

Check warning on line 898 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L897-L898

Added lines #L897 - L898 were not covered by tests
f"fixture function has more than one 'yield':\n\n"
f"{Source(fixturefunc).indent()}\n"
f"{fs}:{lineno + 1}",
pytrace=False,
)


def _eval_scope_callable(
Expand Down
Loading