diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index a21e711fc..e3737356e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -100,15 +100,18 @@ class CodeLocation(NamedTuple): lineno: int +# TODO: integrate after pytest 3.6.0 has been dropped def CodeLocation__str__(self: CodeLocation) -> str: - """Python 3.6 hack for NamedTuple __str__""" + """Python 3.6.0 hack for NamedTuple __str__""" return f"{self.path}:{self.lineno}" setattr(CodeLocation, "__str__", CodeLocation__str__) -def getlocation(function, curdir: Path | None) -> CodeLocation: +def getlocation( + function, *, relative_to: Path | None, allow_escape: bool +) -> CodeLocation: function = get_real_func(function) fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -116,9 +119,12 @@ def getlocation(function, curdir: Path | None) -> CodeLocation: # TODO: this cycle indicates a larger issue from .pathlib import bestrelpath - if curdir is not None: + if relative_to is not None: try: - relfn = Path(bestrelpath(curdir, fn)) + if allow_escape: + relfn = Path(bestrelpath(relative_to, fn)) + else: + relfn = relative_to.relative_to(relative_to) except ValueError: pass else: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 83ee0df5d..6b5c87c77 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -660,7 +660,11 @@ class FixtureRequest: "\n\nRequested here:\n{}:{}".format( funcitem.nodeid, fixturedef.argname, - getlocation(fixturedef.func, funcitem.config.rootpath), + getlocation( + fixturedef.func, + relative_to=funcitem.config.rootpath, + allow_escape=True, + ), source_path_str, source_lineno, ) @@ -1210,7 +1214,7 @@ class FixtureFunctionMarker: name = self.name or function.__name__ if name == "request": - location = getlocation(function, None) + location = getlocation(function, relative_to=None, allow_escape=True) fail( "'request' is a reserved word for fixtures, use another name:\n {}".format( location diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c1ce9b5ea..fdd327457 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1576,9 +1576,8 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") - def get_best_relpath(func) -> str: - loc = getlocation(func, str(curdir)) - return bestrelpath(curdir, Path(loc)) + def get_best_relpath(func): + return getlocation(func, curdir) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: argname = fixture_def.argname @@ -1599,9 +1598,12 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: def write_item(item: nodes.Item) -> None: # Not all items have _fixtureinfo attribute. info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None) + function: Optional[Callable[..., Any]] = getattr(item, "function", None) if info is None or not info.name2fixturedefs: # This test item does not use any fixtures. return + if function is None: + return tw.line() tw.sep("-", f"fixtures used by {item.name}") # TODO: Fix this type ignore. @@ -1642,7 +1644,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if not fixturedefs: continue for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, curdir) + loc = getlocation(fixturedef.func, relative_to=curdir, allow_escape=True) if (fixturedef.argname, loc) in seen: continue seen.add((fixturedef.argname, loc)) @@ -1668,7 +1670,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: continue tw.write(f"{argname}", green=True) if fixturedef.scope != "function": - tw.write(" [%s scope]" % fixturedef.scope, cyan=True) + tw.write(f" [{fixturedef.scope} scope]", cyan=True) tw.write(f" -- {prettypath}", yellow=True) tw.write("\n") doc = inspect.getdoc(fixturedef.func)