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:
parent
443aa0219c
commit
93a5cbf56a
|
@ -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``.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue