Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions doc/en/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

Expand Down
33 changes: 18 additions & 15 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ast
import inspect
import os
import re
import sys
import traceback
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -218,7 +219,7 @@ def relline(self) -> int:
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":
Expand All @@ -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]:
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part also fixes the sybil failure - it passes a string absolute path to cut, which used to work because py.path.local('/foo') == '/foo' but Path('/foo') != '/foo'. So the code now compares strings.

Even if we go with the breaking change option we should get this included in pytest 7.0 (this part is non breaking)

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.

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 21 additions & 0 deletions src/_pytest/legacypath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions testing/code/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
Expand Down
16 changes: 8 additions & 8 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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"

Expand Down Expand Up @@ -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()}}:"


Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions testing/python/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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])


Expand Down
7 changes: 7 additions & 0 deletions testing/test_legacypath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]