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:
|
- `#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``.
|
- ``_pytest.code.Code`` has a new attribute ``source_path`` which returns ``Path`` as an alternative to ``path`` which returns ``py.path.local``.
|
||||||
- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``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.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``.
|
||||||
- The ``_pytest.python.path_matches_patterns()`` function takes ``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 ast
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -83,7 +84,7 @@ class Code:
|
||||||
return self.raw.co_name
|
return self.raw.co_name
|
||||||
|
|
||||||
@property
|
@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
|
"""Return a path object pointing to source code, or an ``str`` in
|
||||||
case of ``OSError`` / non-existing file."""
|
case of ``OSError`` / non-existing file."""
|
||||||
if not self.raw.co_filename:
|
if not self.raw.co_filename:
|
||||||
|
@ -218,7 +219,7 @@ class TracebackEntry:
|
||||||
return self.lineno - self.frame.code.firstlineno
|
return self.lineno - self.frame.code.firstlineno
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
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
|
@property
|
||||||
def statement(self) -> "Source":
|
def statement(self) -> "Source":
|
||||||
|
@ -228,9 +229,9 @@ class TracebackEntry:
|
||||||
return source.getstatement(self.lineno)
|
return source.getstatement(self.lineno)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> Union[Path, str]:
|
def source_path(self) -> Union[Path, str]:
|
||||||
"""Path to the source code."""
|
"""Path to the source code."""
|
||||||
return self.frame.code.path
|
return self.frame.code.source_path
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locals(self) -> Dict[str, Any]:
|
def locals(self) -> Dict[str, Any]:
|
||||||
|
@ -251,7 +252,7 @@ class TracebackEntry:
|
||||||
return None
|
return None
|
||||||
key = astnode = None
|
key = astnode = None
|
||||||
if astcache is not None:
|
if astcache is not None:
|
||||||
key = self.frame.code.path
|
key = self.frame.code.source_path
|
||||||
if key is not None:
|
if key is not None:
|
||||||
astnode = astcache.get(key, None)
|
astnode = astcache.get(key, None)
|
||||||
start = self.getfirstlinesource()
|
start = self.getfirstlinesource()
|
||||||
|
@ -307,7 +308,7 @@ class TracebackEntry:
|
||||||
# but changing it to do so would break certain plugins. See
|
# but changing it to do so would break certain plugins. See
|
||||||
# https://github.com/pytest-dev/pytest/pull/7535/ for details.
|
# https://github.com/pytest-dev/pytest/pull/7535/ for details.
|
||||||
return " File %r:%d in %s\n %s\n" % (
|
return " File %r:%d in %s\n %s\n" % (
|
||||||
str(self.path),
|
str(self.source_path),
|
||||||
self.lineno + 1,
|
self.lineno + 1,
|
||||||
name,
|
name,
|
||||||
line,
|
line,
|
||||||
|
@ -343,10 +344,10 @@ class Traceback(List[TracebackEntry]):
|
||||||
|
|
||||||
def cut(
|
def cut(
|
||||||
self,
|
self,
|
||||||
path: Optional[Union[Path, str]] = None,
|
path: Optional[Union["os.PathLike[str]", str]] = None,
|
||||||
lineno: Optional[int] = None,
|
lineno: Optional[int] = None,
|
||||||
firstlineno: Optional[int] = None,
|
firstlineno: Optional[int] = None,
|
||||||
excludepath: Optional[Path] = None,
|
excludepath: Optional["os.PathLike[str]"] = None,
|
||||||
) -> "Traceback":
|
) -> "Traceback":
|
||||||
"""Return a Traceback instance wrapping part of this 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
|
for formatting reasons (removing some uninteresting bits that deal
|
||||||
with handling of the exception/traceback).
|
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:
|
for x in self:
|
||||||
code = x.frame.code
|
code = x.frame.code
|
||||||
codepath = code.path
|
codepath = code.source_path
|
||||||
if path is not None and codepath != path:
|
if path is not None and str(codepath) != path_:
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
excludepath is not None
|
excludepath is not None
|
||||||
and isinstance(codepath, Path)
|
and isinstance(codepath, Path)
|
||||||
and excludepath in codepath.parents
|
and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if lineno is not None and x.lineno != lineno:
|
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
|
# the strange metaprogramming in the decorator lib from pypi
|
||||||
# which generates code objects that have hash/value equality
|
# which generates code objects that have hash/value equality
|
||||||
# XXX needs a test
|
# 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
|
# print "checking for recursion at", key
|
||||||
values = cache.setdefault(key, [])
|
values = cache.setdefault(key, [])
|
||||||
if values:
|
if values:
|
||||||
|
@ -818,7 +821,7 @@ class FormattedExcinfo:
|
||||||
message = "in %s" % (entry.name)
|
message = "in %s" % (entry.name)
|
||||||
else:
|
else:
|
||||||
message = excinfo and excinfo.typename or ""
|
message = excinfo and excinfo.typename or ""
|
||||||
entry_path = entry.path
|
entry_path = entry.source_path
|
||||||
path = self._makepath(entry_path)
|
path = self._makepath(entry_path)
|
||||||
reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
|
reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
|
||||||
localsrepr = self.repr_locals(entry.locals)
|
localsrepr = self.repr_locals(entry.locals)
|
||||||
|
@ -1227,7 +1230,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
|
||||||
pass
|
pass
|
||||||
return fspath, lineno
|
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;
|
# 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
|
# entry.path might point to a non-existing file, in which case it will
|
||||||
# also return a str object. See #1133.
|
# also return a str object. See #1133.
|
||||||
p = Path(entry.path)
|
p = Path(entry.source_path)
|
||||||
|
|
||||||
parents = p.parents
|
parents = p.parents
|
||||||
if _PLUGGY_DIR in 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
|
Make a special case for importlib because we use it to import test modules and conftest files
|
||||||
in _pytest.pathlib.import_path.
|
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(
|
def main(
|
||||||
|
|
|
@ -11,6 +11,8 @@ import attr
|
||||||
from iniconfig import SectionWrapper
|
from iniconfig import SectionWrapper
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from _pytest._code import Code
|
||||||
|
from _pytest._code import TracebackEntry
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
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)
|
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
|
@pytest.hookimpl
|
||||||
def pytest_configure(config: pytest.Config) -> None:
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
mp = pytest.MonkeyPatch()
|
mp = pytest.MonkeyPatch()
|
||||||
|
@ -451,6 +466,12 @@ def pytest_configure(config: pytest.Config) -> None:
|
||||||
# Add Node.fspath property.
|
# Add Node.fspath property.
|
||||||
mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
|
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
|
@pytest.hookimpl
|
||||||
def pytest_plugin_registered(
|
def pytest_plugin_registered(
|
||||||
|
|
|
@ -1721,7 +1721,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||||
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
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
|
traceback = excinfo.traceback
|
||||||
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
||||||
if ntraceback == traceback:
|
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")
|
co_code = compile("pass\n", name, "exec")
|
||||||
assert co_code.co_filename == name
|
assert co_code.co_filename == name
|
||||||
code = Code(co_code)
|
code = Code(co_code)
|
||||||
assert str(code.path) == name
|
assert str(code.source_path) == name
|
||||||
assert code.fullsource is None
|
assert code.fullsource is None
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ def test_getstatement_empty_fullsource() -> None:
|
||||||
def test_code_from_func() -> None:
|
def test_code_from_func() -> None:
|
||||||
co = Code.from_function(test_frame_getsourcelineno_myself)
|
co = Code.from_function(test_frame_getsourcelineno_myself)
|
||||||
assert co.firstlineno
|
assert co.firstlineno
|
||||||
assert co.path
|
assert co.source_path
|
||||||
|
|
||||||
|
|
||||||
def test_unicode_handling() -> None:
|
def test_unicode_handling() -> None:
|
||||||
|
|
|
@ -151,7 +151,7 @@ class TestTraceback_f_g_h:
|
||||||
|
|
||||||
def test_traceback_cut(self) -> None:
|
def test_traceback_cut(self) -> None:
|
||||||
co = _pytest._code.Code.from_function(f)
|
co = _pytest._code.Code.from_function(f)
|
||||||
path, firstlineno = co.path, co.firstlineno
|
path, firstlineno = co.source_path, co.firstlineno
|
||||||
assert isinstance(path, Path)
|
assert isinstance(path, Path)
|
||||||
traceback = self.excinfo.traceback
|
traceback = self.excinfo.traceback
|
||||||
newtraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
newtraceback = traceback.cut(path=path, firstlineno=firstlineno)
|
||||||
|
@ -166,9 +166,9 @@ class TestTraceback_f_g_h:
|
||||||
basedir = Path(pytest.__file__).parent
|
basedir = Path(pytest.__file__).parent
|
||||||
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
||||||
for x in newtraceback:
|
for x in newtraceback:
|
||||||
assert isinstance(x.path, Path)
|
assert isinstance(x.source_path, Path)
|
||||||
assert basedir not in x.path.parents
|
assert basedir not in x.source_path.parents
|
||||||
assert newtraceback[-1].frame.code.path == p
|
assert newtraceback[-1].frame.code.source_path == p
|
||||||
|
|
||||||
def test_traceback_filter(self):
|
def test_traceback_filter(self):
|
||||||
traceback = self.excinfo.traceback
|
traceback = self.excinfo.traceback
|
||||||
|
@ -295,7 +295,7 @@ class TestTraceback_f_g_h:
|
||||||
tb = excinfo.traceback
|
tb = excinfo.traceback
|
||||||
entry = tb.getcrashentry()
|
entry = tb.getcrashentry()
|
||||||
co = _pytest._code.Code.from_function(h)
|
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.lineno == co.firstlineno + 1
|
||||||
assert entry.frame.code.name == "h"
|
assert entry.frame.code.name == "h"
|
||||||
|
|
||||||
|
@ -312,7 +312,7 @@ class TestTraceback_f_g_h:
|
||||||
tb = excinfo.traceback
|
tb = excinfo.traceback
|
||||||
entry = tb.getcrashentry()
|
entry = tb.getcrashentry()
|
||||||
co = _pytest._code.Code.from_function(g)
|
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.lineno == co.firstlineno + 2
|
||||||
assert entry.frame.code.name == "g"
|
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:
|
for item in excinfo.traceback:
|
||||||
print(item) # XXX: for some reason jinja.Template.render is printed in full
|
print(item) # XXX: for some reason jinja.Template.render is printed in full
|
||||||
item.source # shouldn't fail
|
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()}}:"
|
assert str(item.source) == "{{ h()}}:"
|
||||||
|
|
||||||
|
|
||||||
|
@ -398,7 +398,7 @@ def test_codepath_Queue_example() -> None:
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
excinfo = _pytest._code.ExceptionInfo.from_current()
|
excinfo = _pytest._code.ExceptionInfo.from_current()
|
||||||
entry = excinfo.traceback[-1]
|
entry = excinfo.traceback[-1]
|
||||||
path = entry.path
|
path = entry.source_path
|
||||||
assert isinstance(path, Path)
|
assert isinstance(path, Path)
|
||||||
assert path.name.lower() == "queue.py"
|
assert path.name.lower() == "queue.py"
|
||||||
assert path.exists()
|
assert path.exists()
|
||||||
|
|
|
@ -1102,7 +1102,7 @@ class TestTracebackCutting:
|
||||||
|
|
||||||
assert tb is not None
|
assert tb is not None
|
||||||
traceback = _pytest._code.Traceback(tb)
|
traceback = _pytest._code.Traceback(tb)
|
||||||
assert isinstance(traceback[-1].path, str)
|
assert isinstance(traceback[-1].source_path, str)
|
||||||
assert not filter_traceback(traceback[-1])
|
assert not filter_traceback(traceback[-1])
|
||||||
|
|
||||||
def test_filter_traceback_path_no_longer_valid(self, pytester: Pytester) -> None:
|
def test_filter_traceback_path_no_longer_valid(self, pytester: Pytester) -> None:
|
||||||
|
@ -1132,7 +1132,7 @@ class TestTracebackCutting:
|
||||||
assert tb is not None
|
assert tb is not None
|
||||||
pytester.path.joinpath("filter_traceback_entry_as_str.py").unlink()
|
pytester.path.joinpath("filter_traceback_entry_as_str.py").unlink()
|
||||||
traceback = _pytest._code.Traceback(tb)
|
traceback = _pytest._code.Traceback(tb)
|
||||||
assert isinstance(traceback[-1].path, str)
|
assert isinstance(traceback[-1].source_path, str)
|
||||||
assert filter_traceback(traceback[-1])
|
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 = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s")
|
||||||
result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"])
|
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