diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index c5ccc75e3af..c8c715d354d 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -473,8 +473,8 @@ Trivial/Internal Changes - `#8174 `_: The following changes have been made to internal pytest types/functions: - - The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``. - - The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``. + - ``_pytest.code.Code`` has a new attribute ``source_path`` which returns ``Path`` as an alternative to ``path`` which returns ``py.path.local``. + - ``_pytest.code.TracebackEntry`` has a new attribute ``source_path`` which returns ``Path`` as an alternative to ``path`` which returns ``py.path.local``. - The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``. - The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b19ee7c64d9..7ce824cc0b7 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1,5 +1,6 @@ import ast import inspect +import os import re import sys import traceback @@ -83,7 +84,7 @@ def name(self) -> str: return self.raw.co_name @property - def path(self) -> Union[Path, str]: + def source_path(self) -> Union[Path, str]: """Return a path object pointing to source code, or an ``str`` in case of ``OSError`` / non-existing file.""" if not self.raw.co_filename: @@ -218,7 +219,7 @@ def relline(self) -> int: return self.lineno - self.frame.code.firstlineno def __repr__(self) -> str: - return "" % (self.frame.code.path, self.lineno + 1) + return "" % (self.frame.code.source_path, self.lineno + 1) @property def statement(self) -> "Source": @@ -228,9 +229,9 @@ def statement(self) -> "Source": return source.getstatement(self.lineno) @property - def path(self) -> Union[Path, str]: + def source_path(self) -> Union[Path, str]: """Path to the source code.""" - return self.frame.code.path + return self.frame.code.source_path @property def locals(self) -> Dict[str, Any]: @@ -251,7 +252,7 @@ def getsource( return None key = astnode = None if astcache is not None: - key = self.frame.code.path + key = self.frame.code.source_path if key is not None: astnode = astcache.get(key, None) start = self.getfirstlinesource() @@ -307,7 +308,7 @@ def __str__(self) -> str: # but changing it to do so would break certain plugins. See # https://github.com/pytest-dev/pytest/pull/7535/ for details. return " File %r:%d in %s\n %s\n" % ( - str(self.path), + str(self.source_path), self.lineno + 1, name, line, @@ -343,10 +344,10 @@ def f(cur: TracebackType) -> Iterable[TracebackEntry]: def cut( self, - path: Optional[Union[Path, str]] = None, + path: Optional[Union["os.PathLike[str]", str]] = None, lineno: Optional[int] = None, firstlineno: Optional[int] = None, - excludepath: Optional[Path] = None, + excludepath: Optional["os.PathLike[str]"] = None, ) -> "Traceback": """Return a Traceback instance wrapping part of this Traceback. @@ -357,15 +358,17 @@ def cut( for formatting reasons (removing some uninteresting bits that deal with handling of the exception/traceback). """ + path_ = None if path is None else os.fspath(path) + excludepath_ = None if excludepath is None else os.fspath(excludepath) for x in self: code = x.frame.code - codepath = code.path - if path is not None and codepath != path: + codepath = code.source_path + if path is not None and str(codepath) != path_: continue if ( excludepath is not None and isinstance(codepath, Path) - and excludepath in codepath.parents + and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator] ): continue if lineno is not None and x.lineno != lineno: @@ -422,7 +425,7 @@ def recursionindex(self) -> Optional[int]: # the strange metaprogramming in the decorator lib from pypi # which generates code objects that have hash/value equality # XXX needs a test - key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno + key = entry.frame.code.source_path, id(entry.frame.code.raw), entry.lineno # print "checking for recursion at", key values = cache.setdefault(key, []) if values: @@ -818,7 +821,7 @@ def repr_traceback_entry( message = "in %s" % (entry.name) else: message = excinfo and excinfo.typename or "" - entry_path = entry.path + entry_path = entry.source_path path = self._makepath(entry_path) reprfileloc = ReprFileLocation(path, entry.lineno + 1, message) localsrepr = self.repr_locals(entry.locals) @@ -1227,7 +1230,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: pass return fspath, lineno - return code.path, code.firstlineno + return code.source_path, code.firstlineno # Relative paths that we use to filter traceback entries from appearing to the user; @@ -1260,7 +1263,7 @@ def filter_traceback(entry: TracebackEntry) -> bool: # entry.path might point to a non-existing file, in which case it will # also return a str object. See #1133. - p = Path(entry.path) + p = Path(entry.source_path) parents = p.parents if _PLUGGY_DIR in parents: diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4cb22009a06..4492fe82120 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -127,7 +127,9 @@ def filter_traceback_for_conftest_import_failure( Make a special case for importlib because we use it to import test modules and conftest files in _pytest.pathlib.import_path. """ - return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep) + return filter_traceback(entry) and "importlib" not in str(entry.source_path).split( + os.sep + ) def main( diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 743c06d55c4..72edf623d16 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -11,6 +11,8 @@ from iniconfig import SectionWrapper import pytest +from _pytest._code import Code +from _pytest._code import TracebackEntry from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path @@ -400,6 +402,19 @@ def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: self.path = Path(value) +def Code_path(self: Code) -> Union[str, LEGACY_PATH]: + """Return a path object pointing to source code, or an ``str`` in + case of ``OSError`` / non-existing file.""" + path = self.source_path + return path if isinstance(path, str) else legacy_path(path) + + +def TracebackEntry_path(self: TracebackEntry) -> Union[str, LEGACY_PATH]: + """Path to the source code.""" + path = self.source_path + return path if isinstance(path, str) else legacy_path(path) + + @pytest.hookimpl def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() @@ -451,6 +466,12 @@ def pytest_configure(config: pytest.Config) -> None: # Add Node.fspath property. mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) + # Add Code.path property. + mp.setattr(Code, "path", property(Code_path), raising=False) + + # Add TracebackEntry.path property. + mp.setattr(TracebackEntry, "path", property(TracebackEntry_path), raising=False) + @pytest.hookimpl def pytest_plugin_registered( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index aa49aa26499..0771bffe39d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1721,7 +1721,7 @@ def setup(self) -> None: def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): code = _pytest._code.Code.from_function(get_real_func(self.obj)) - path, firstlineno = code.path, code.firstlineno + path, firstlineno = code.source_path, code.firstlineno traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) if ntraceback == traceback: diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 33809528a06..c73be90dd89 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -24,7 +24,7 @@ def test_code_gives_back_name_for_not_existing_file() -> None: co_code = compile("pass\n", name, "exec") assert co_code.co_filename == name code = Code(co_code) - assert str(code.path) == name + assert str(code.source_path) == name assert code.fullsource is None @@ -76,7 +76,7 @@ def func() -> FrameType: def test_code_from_func() -> None: co = Code.from_function(test_frame_getsourcelineno_myself) assert co.firstlineno - assert co.path + assert co.source_path def test_unicode_handling() -> None: diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 61aa4406ad2..0b386bca127 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -151,7 +151,7 @@ def xyz(): def test_traceback_cut(self) -> None: co = _pytest._code.Code.from_function(f) - path, firstlineno = co.path, co.firstlineno + path, firstlineno = co.source_path, co.firstlineno assert isinstance(path, Path) traceback = self.excinfo.traceback newtraceback = traceback.cut(path=path, firstlineno=firstlineno) @@ -166,9 +166,9 @@ def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: basedir = Path(pytest.__file__).parent newtraceback = excinfo.traceback.cut(excludepath=basedir) for x in newtraceback: - assert isinstance(x.path, Path) - assert basedir not in x.path.parents - assert newtraceback[-1].frame.code.path == p + assert isinstance(x.source_path, Path) + assert basedir not in x.source_path.parents + assert newtraceback[-1].frame.code.source_path == p def test_traceback_filter(self): traceback = self.excinfo.traceback @@ -295,7 +295,7 @@ def f(): tb = excinfo.traceback entry = tb.getcrashentry() co = _pytest._code.Code.from_function(h) - assert entry.frame.code.path == co.path + assert entry.frame.code.source_path == co.source_path assert entry.lineno == co.firstlineno + 1 assert entry.frame.code.name == "h" @@ -312,7 +312,7 @@ def f(): tb = excinfo.traceback entry = tb.getcrashentry() co = _pytest._code.Code.from_function(g) - assert entry.frame.code.path == co.path + assert entry.frame.code.source_path == co.source_path assert entry.lineno == co.firstlineno + 2 assert entry.frame.code.name == "g" @@ -376,7 +376,7 @@ def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: for item in excinfo.traceback: print(item) # XXX: for some reason jinja.Template.render is printed in full item.source # shouldn't fail - if isinstance(item.path, Path) and item.path.name == "test.txt": + if isinstance(item.source_path, Path) and item.source_path.name == "test.txt": assert str(item.source) == "{{ h()}}:" @@ -398,7 +398,7 @@ def test_codepath_Queue_example() -> None: except queue.Empty: excinfo = _pytest._code.ExceptionInfo.from_current() entry = excinfo.traceback[-1] - path = entry.path + path = entry.source_path assert isinstance(path, Path) assert path.name.lower() == "queue.py" assert path.exists() diff --git a/testing/python/collect.py b/testing/python/collect.py index ac3edd395ab..2d7fb453da5 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1102,7 +1102,7 @@ def test_filter_traceback_generated_code(self) -> None: assert tb is not None traceback = _pytest._code.Traceback(tb) - assert isinstance(traceback[-1].path, str) + assert isinstance(traceback[-1].source_path, str) assert not filter_traceback(traceback[-1]) def test_filter_traceback_path_no_longer_valid(self, pytester: Pytester) -> None: @@ -1132,7 +1132,7 @@ def foo(): assert tb is not None pytester.path.joinpath("filter_traceback_entry_as_str.py").unlink() traceback = _pytest._code.Traceback(tb) - assert isinstance(traceback[-1].path, str) + assert isinstance(traceback[-1].source_path, str) assert filter_traceback(traceback[-1]) diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 9ab139df467..86f01cfef21 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -161,3 +161,10 @@ def test_overriden(pytestconfig): ) result = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s") result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"]) + + +def test_code_path() -> None: + with pytest.raises(Exception) as excinfo: + raise Exception() + assert isinstance(excinfo.traceback[0].path, LEGACY_PATH) # type: ignore[attr-defined] + assert isinstance(excinfo.traceback[0].frame.code.path, LEGACY_PATH) # type: ignore[attr-defined]