Restore {Code,TracebackEntry}.path to py.path and add alternative

In 92ba96b061 we have changed the `path`
attribute to return a `pathlib.Path` instead of `py.path.local` without
a deprecation hoping it would be alright. But these types are somewhat
public, reachable through `ExceptionInfo.traceback`, and broke code in
practice. So restore them in the legacypath plugin and add `Path`
alternatives under a different name - `source_path`.

Fix #9423.
This commit is contained in:
Ran Benita 2021-12-25 11:15:34 +02:00
parent 443aa0219c
commit 93a5cbf56a
9 changed files with 64 additions and 31 deletions

View File

@ -473,8 +473,8 @@ Trivial/Internal Changes
- `#8174 <https://github.com/pytest-dev/pytest/issues/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``.

View File

@ -1,5 +1,6 @@
import ast
import inspect
import os
import re
import sys
import traceback
@ -83,7 +84,7 @@ class Code:
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 @@ class TracebackEntry:
return self.lineno - self.frame.code.firstlineno
def __repr__(self) -> str:
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
return "<TracebackEntry %s:%d>" % (self.frame.code.source_path, self.lineno + 1)
@property
def statement(self) -> "Source":
@ -228,9 +229,9 @@ class TracebackEntry:
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 @@ class TracebackEntry:
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 @@ class TracebackEntry:
# 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 @@ class Traceback(List[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 @@ class Traceback(List[TracebackEntry]):
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 @@ class Traceback(List[TracebackEntry]):
# 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 @@ class FormattedExcinfo:
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:

View File

@ -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(

View File

@ -11,6 +11,8 @@ import attr
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(

View File

@ -1721,7 +1721,7 @@ class Function(PyobjMixin, nodes.Item):
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:

View File

@ -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 test_getstatement_empty_fullsource() -> None:
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:

View File

@ -151,7 +151,7 @@ class TestTraceback_f_g_h:
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 @@ class TestTraceback_f_g_h:
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 @@ class TestTraceback_f_g_h:
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 @@ class TestTraceback_f_g_h:
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()

View File

@ -1102,7 +1102,7 @@ class TestTracebackCutting:
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 @@ class TestTracebackCutting:
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])

View File

@ -161,3 +161,10 @@ def test_override_ini_paths(pytester: pytest.Pytester) -> None:
)
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]