From a99885f3449f19b37ee8e90aabc7cfc0382aadde Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sat, 21 Nov 2020 14:17:31 +0100 Subject: [PATCH 1/3] [RFC] code location as a namedtuple --- src/_pytest/compat.py | 25 ++++++++++++++++++++++--- src/_pytest/fixtures.py | 2 +- src/_pytest/python.py | 5 +++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 352211de8..a21e711fc 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Generic +from typing import NamedTuple from typing import NoReturn from typing import TYPE_CHECKING from typing import TypeVar @@ -94,17 +95,35 @@ def is_async_function(func: object) -> bool: def getlocation(function, curdir: str | None = None) -> str: +class CodeLocation(NamedTuple): + path: Path + lineno: int + + +def CodeLocation__str__(self: CodeLocation) -> str: + """Python 3.6 hack for NamedTuple __str__""" + return f"{self.path}:{self.lineno}" + + +setattr(CodeLocation, "__str__", CodeLocation__str__) + + +def getlocation(function, curdir: Path | None) -> CodeLocation: function = get_real_func(function) fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno + + # TODO: this cycle indicates a larger issue + from .pathlib import bestrelpath + if curdir is not None: try: - relfn = fn.relative_to(curdir) + relfn = Path(bestrelpath(curdir, fn)) except ValueError: pass else: - return "%s:%d" % (relfn, lineno + 1) - return "%s:%d" % (fn, lineno + 1) + return CodeLocation(relfn, lineno + 1) + return CodeLocation(fn, lineno + 1) def num_mock_patch_args(function) -> int: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 007245b24..83ee0df5d 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1210,7 +1210,7 @@ class FixtureFunctionMarker: name = self.name or function.__name__ if name == "request": - location = getlocation(function) + location = getlocation(function, None) 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 d04b6fa4d..c1ce9b5ea 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -39,6 +39,7 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped from _pytest.compat import assert_never +from _pytest.compat import CodeLocation from _pytest.compat import final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func @@ -1634,14 +1635,14 @@ def _showfixtures_main(config: Config, session: Session) -> None: fm = session._fixturemanager available = [] - seen: Set[Tuple[str, str]] = set() + seen: Set[Tuple[str, CodeLocation]] = set() for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None if not fixturedefs: continue for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, str(curdir)) + loc = getlocation(fixturedef.func, curdir) if (fixturedef.argname, loc) in seen: continue seen.add((fixturedef.argname, loc)) From dfde2c6c343285534972913e82d5d3582e5757d0 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Sun, 22 Nov 2020 20:09:45 +0100 Subject: [PATCH 2/3] WIP: experiment with allow_escape for getlocation --- src/_pytest/compat.py | 14 ++++++++++---- src/_pytest/fixtures.py | 8 ++++++-- src/_pytest/python.py | 12 +++++++----- 3 files changed, 23 insertions(+), 11 deletions(-) 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) From 02ad46a5a43bada7c787083099f1cf0c37a96ec1 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 17 Mar 2023 09:58:27 +0100 Subject: [PATCH 3/3] fixup: correct rebase mistakes, to be squashed --- src/_pytest/compat.py | 12 +++--------- src/_pytest/python.py | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index e3737356e..26b6e95c2 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -94,19 +94,13 @@ def is_async_function(func: object) -> bool: return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) -def getlocation(function, curdir: str | None = None) -> str: class CodeLocation(NamedTuple): path: Path lineno: int - -# TODO: integrate after pytest 3.6.0 has been dropped -def CodeLocation__str__(self: CodeLocation) -> str: - """Python 3.6.0 hack for NamedTuple __str__""" - return f"{self.path}:{self.lineno}" - - -setattr(CodeLocation, "__str__", CodeLocation__str__) + def __str__(self: CodeLocation) -> str: + """Python 3.6.0 hack for NamedTuple __str__""" + return f"{self.path}:{self.lineno}" def getlocation( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index fdd327457..949dbd2db 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1554,7 +1554,7 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) - def _pretty_fixture_path(func) -> str: cwd = Path.cwd() - loc = Path(getlocation(func, str(cwd))) + loc = Path(str(getlocation(func, relative_to=cwd, allow_escape=False))) prefix = Path("...", "_pytest") try: return str(prefix / loc.relative_to(_PYTEST_DIR)) @@ -1577,7 +1577,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: verbose = config.getvalue("verbose") def get_best_relpath(func): - return getlocation(func, curdir) + return getlocation(func, relative_to=curdir, allow_escape=False) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: argname = fixture_def.argname