Skip to content

Commit 93a5cbf

Browse files
committed
Restore {Code,TracebackEntry}.path to py.path and add alternative
In 92ba96b 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 pytest-dev#9423.
1 parent 443aa02 commit 93a5cbf

File tree

9 files changed

+64
-31
lines changed

9 files changed

+64
-31
lines changed

doc/en/changelog.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,8 @@ Trivial/Internal Changes
473473

474474
- `#8174 <https://github.com/pytest-dev/pytest/issues/8174>`_: The following changes have been made to internal pytest types/functions:
475475

476-
- The ``path`` property of ``_pytest.code.Code`` returns ``Path`` instead of ``py.path.local``.
477-
- The ``path`` property of ``_pytest.code.TracebackEntry`` returns ``Path`` instead of ``py.path.local``.
476+
- ``_pytest.code.Code`` has a new attribute ``source_path`` which returns ``Path`` as an alternative to ``path`` which returns ``py.path.local``.
477+
- ``_pytest.code.TracebackEntry`` has a new attribute ``source_path`` which returns ``Path`` as an alternative to ``path`` which returns ``py.path.local``.
478478
- The ``_pytest.code.getfslineno()`` function returns ``Path`` instead of ``py.path.local``.
479479
- The ``_pytest.python.path_matches_patterns()`` function takes ``Path`` instead of ``py.path.local``.
480480

src/_pytest/_code/code.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ast
22
import inspect
3+
import os
34
import re
45
import sys
56
import traceback
@@ -83,7 +84,7 @@ def name(self) -> str:
8384
return self.raw.co_name
8485

8586
@property
86-
def path(self) -> Union[Path, str]:
87+
def source_path(self) -> Union[Path, str]:
8788
"""Return a path object pointing to source code, or an ``str`` in
8889
case of ``OSError`` / non-existing file."""
8990
if not self.raw.co_filename:
@@ -218,7 +219,7 @@ def relline(self) -> int:
218219
return self.lineno - self.frame.code.firstlineno
219220

220221
def __repr__(self) -> str:
221-
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
222+
return "<TracebackEntry %s:%d>" % (self.frame.code.source_path, self.lineno + 1)
222223

223224
@property
224225
def statement(self) -> "Source":
@@ -228,9 +229,9 @@ def statement(self) -> "Source":
228229
return source.getstatement(self.lineno)
229230

230231
@property
231-
def path(self) -> Union[Path, str]:
232+
def source_path(self) -> Union[Path, str]:
232233
"""Path to the source code."""
233-
return self.frame.code.path
234+
return self.frame.code.source_path
234235

235236
@property
236237
def locals(self) -> Dict[str, Any]:
@@ -251,7 +252,7 @@ def getsource(
251252
return None
252253
key = astnode = None
253254
if astcache is not None:
254-
key = self.frame.code.path
255+
key = self.frame.code.source_path
255256
if key is not None:
256257
astnode = astcache.get(key, None)
257258
start = self.getfirstlinesource()
@@ -307,7 +308,7 @@ def __str__(self) -> str:
307308
# but changing it to do so would break certain plugins. See
308309
# https://github.com/pytest-dev/pytest/pull/7535/ for details.
309310
return " File %r:%d in %s\n %s\n" % (
310-
str(self.path),
311+
str(self.source_path),
311312
self.lineno + 1,
312313
name,
313314
line,
@@ -343,10 +344,10 @@ def f(cur: TracebackType) -> Iterable[TracebackEntry]:
343344

344345
def cut(
345346
self,
346-
path: Optional[Union[Path, str]] = None,
347+
path: Optional[Union["os.PathLike[str]", str]] = None,
347348
lineno: Optional[int] = None,
348349
firstlineno: Optional[int] = None,
349-
excludepath: Optional[Path] = None,
350+
excludepath: Optional["os.PathLike[str]"] = None,
350351
) -> "Traceback":
351352
"""Return a Traceback instance wrapping part of this Traceback.
352353
@@ -357,15 +358,17 @@ def cut(
357358
for formatting reasons (removing some uninteresting bits that deal
358359
with handling of the exception/traceback).
359360
"""
361+
path_ = None if path is None else os.fspath(path)
362+
excludepath_ = None if excludepath is None else os.fspath(excludepath)
360363
for x in self:
361364
code = x.frame.code
362-
codepath = code.path
363-
if path is not None and codepath != path:
365+
codepath = code.source_path
366+
if path is not None and str(codepath) != path_:
364367
continue
365368
if (
366369
excludepath is not None
367370
and isinstance(codepath, Path)
368-
and excludepath in codepath.parents
371+
and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
369372
):
370373
continue
371374
if lineno is not None and x.lineno != lineno:
@@ -422,7 +425,7 @@ def recursionindex(self) -> Optional[int]:
422425
# the strange metaprogramming in the decorator lib from pypi
423426
# which generates code objects that have hash/value equality
424427
# XXX needs a test
425-
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
428+
key = entry.frame.code.source_path, id(entry.frame.code.raw), entry.lineno
426429
# print "checking for recursion at", key
427430
values = cache.setdefault(key, [])
428431
if values:
@@ -818,7 +821,7 @@ def repr_traceback_entry(
818821
message = "in %s" % (entry.name)
819822
else:
820823
message = excinfo and excinfo.typename or ""
821-
entry_path = entry.path
824+
entry_path = entry.source_path
822825
path = self._makepath(entry_path)
823826
reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
824827
localsrepr = self.repr_locals(entry.locals)
@@ -1227,7 +1230,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
12271230
pass
12281231
return fspath, lineno
12291232

1230-
return code.path, code.firstlineno
1233+
return code.source_path, code.firstlineno
12311234

12321235

12331236
# Relative paths that we use to filter traceback entries from appearing to the user;
@@ -1260,7 +1263,7 @@ def filter_traceback(entry: TracebackEntry) -> bool:
12601263

12611264
# entry.path might point to a non-existing file, in which case it will
12621265
# also return a str object. See #1133.
1263-
p = Path(entry.path)
1266+
p = Path(entry.source_path)
12641267

12651268
parents = p.parents
12661269
if _PLUGGY_DIR in parents:

src/_pytest/config/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ def filter_traceback_for_conftest_import_failure(
127127
Make a special case for importlib because we use it to import test modules and conftest files
128128
in _pytest.pathlib.import_path.
129129
"""
130-
return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
130+
return filter_traceback(entry) and "importlib" not in str(entry.source_path).split(
131+
os.sep
132+
)
131133

132134

133135
def main(

src/_pytest/legacypath.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from iniconfig import SectionWrapper
1212

1313
import pytest
14+
from _pytest._code import Code
15+
from _pytest._code import TracebackEntry
1416
from _pytest.compat import final
1517
from _pytest.compat import LEGACY_PATH
1618
from _pytest.compat import legacy_path
@@ -400,6 +402,19 @@ def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
400402
self.path = Path(value)
401403

402404

405+
def Code_path(self: Code) -> Union[str, LEGACY_PATH]:
406+
"""Return a path object pointing to source code, or an ``str`` in
407+
case of ``OSError`` / non-existing file."""
408+
path = self.source_path
409+
return path if isinstance(path, str) else legacy_path(path)
410+
411+
412+
def TracebackEntry_path(self: TracebackEntry) -> Union[str, LEGACY_PATH]:
413+
"""Path to the source code."""
414+
path = self.source_path
415+
return path if isinstance(path, str) else legacy_path(path)
416+
417+
403418
@pytest.hookimpl
404419
def pytest_configure(config: pytest.Config) -> None:
405420
mp = pytest.MonkeyPatch()
@@ -451,6 +466,12 @@ def pytest_configure(config: pytest.Config) -> None:
451466
# Add Node.fspath property.
452467
mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
453468

469+
# Add Code.path property.
470+
mp.setattr(Code, "path", property(Code_path), raising=False)
471+
472+
# Add TracebackEntry.path property.
473+
mp.setattr(TracebackEntry, "path", property(TracebackEntry_path), raising=False)
474+
454475

455476
@pytest.hookimpl
456477
def pytest_plugin_registered(

src/_pytest/python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,7 @@ def setup(self) -> None:
17211721
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
17221722
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
17231723
code = _pytest._code.Code.from_function(get_real_func(self.obj))
1724-
path, firstlineno = code.path, code.firstlineno
1724+
path, firstlineno = code.source_path, code.firstlineno
17251725
traceback = excinfo.traceback
17261726
ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
17271727
if ntraceback == traceback:

testing/code/test_code.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_code_gives_back_name_for_not_existing_file() -> None:
2424
co_code = compile("pass\n", name, "exec")
2525
assert co_code.co_filename == name
2626
code = Code(co_code)
27-
assert str(code.path) == name
27+
assert str(code.source_path) == name
2828
assert code.fullsource is None
2929

3030

@@ -76,7 +76,7 @@ def func() -> FrameType:
7676
def test_code_from_func() -> None:
7777
co = Code.from_function(test_frame_getsourcelineno_myself)
7878
assert co.firstlineno
79-
assert co.path
79+
assert co.source_path
8080

8181

8282
def test_unicode_handling() -> None:

testing/code/test_excinfo.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def xyz():
151151

152152
def test_traceback_cut(self) -> None:
153153
co = _pytest._code.Code.from_function(f)
154-
path, firstlineno = co.path, co.firstlineno
154+
path, firstlineno = co.source_path, co.firstlineno
155155
assert isinstance(path, Path)
156156
traceback = self.excinfo.traceback
157157
newtraceback = traceback.cut(path=path, firstlineno=firstlineno)
@@ -166,9 +166,9 @@ def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
166166
basedir = Path(pytest.__file__).parent
167167
newtraceback = excinfo.traceback.cut(excludepath=basedir)
168168
for x in newtraceback:
169-
assert isinstance(x.path, Path)
170-
assert basedir not in x.path.parents
171-
assert newtraceback[-1].frame.code.path == p
169+
assert isinstance(x.source_path, Path)
170+
assert basedir not in x.source_path.parents
171+
assert newtraceback[-1].frame.code.source_path == p
172172

173173
def test_traceback_filter(self):
174174
traceback = self.excinfo.traceback
@@ -295,7 +295,7 @@ def f():
295295
tb = excinfo.traceback
296296
entry = tb.getcrashentry()
297297
co = _pytest._code.Code.from_function(h)
298-
assert entry.frame.code.path == co.path
298+
assert entry.frame.code.source_path == co.source_path
299299
assert entry.lineno == co.firstlineno + 1
300300
assert entry.frame.code.name == "h"
301301

@@ -312,7 +312,7 @@ def f():
312312
tb = excinfo.traceback
313313
entry = tb.getcrashentry()
314314
co = _pytest._code.Code.from_function(g)
315-
assert entry.frame.code.path == co.path
315+
assert entry.frame.code.source_path == co.source_path
316316
assert entry.lineno == co.firstlineno + 2
317317
assert entry.frame.code.name == "g"
318318

@@ -376,7 +376,7 @@ def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
376376
for item in excinfo.traceback:
377377
print(item) # XXX: for some reason jinja.Template.render is printed in full
378378
item.source # shouldn't fail
379-
if isinstance(item.path, Path) and item.path.name == "test.txt":
379+
if isinstance(item.source_path, Path) and item.source_path.name == "test.txt":
380380
assert str(item.source) == "{{ h()}}:"
381381

382382

@@ -398,7 +398,7 @@ def test_codepath_Queue_example() -> None:
398398
except queue.Empty:
399399
excinfo = _pytest._code.ExceptionInfo.from_current()
400400
entry = excinfo.traceback[-1]
401-
path = entry.path
401+
path = entry.source_path
402402
assert isinstance(path, Path)
403403
assert path.name.lower() == "queue.py"
404404
assert path.exists()

testing/python/collect.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@ def test_filter_traceback_generated_code(self) -> None:
11021102

11031103
assert tb is not None
11041104
traceback = _pytest._code.Traceback(tb)
1105-
assert isinstance(traceback[-1].path, str)
1105+
assert isinstance(traceback[-1].source_path, str)
11061106
assert not filter_traceback(traceback[-1])
11071107

11081108
def test_filter_traceback_path_no_longer_valid(self, pytester: Pytester) -> None:
@@ -1132,7 +1132,7 @@ def foo():
11321132
assert tb is not None
11331133
pytester.path.joinpath("filter_traceback_entry_as_str.py").unlink()
11341134
traceback = _pytest._code.Traceback(tb)
1135-
assert isinstance(traceback[-1].path, str)
1135+
assert isinstance(traceback[-1].source_path, str)
11361136
assert filter_traceback(traceback[-1])
11371137

11381138

testing/test_legacypath.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,10 @@ def test_overriden(pytestconfig):
161161
)
162162
result = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s")
163163
result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"])
164+
165+
166+
def test_code_path() -> None:
167+
with pytest.raises(Exception) as excinfo:
168+
raise Exception()
169+
assert isinstance(excinfo.traceback[0].path, LEGACY_PATH) # type: ignore[attr-defined]
170+
assert isinstance(excinfo.traceback[0].frame.code.path, LEGACY_PATH) # type: ignore[attr-defined]

0 commit comments

Comments
 (0)