Skip to content

Commit 7eee5c1

Browse files
committed
Change Node.reportinfo() return value from py.path to str|os.PathLike[str]
`reportinfo()` is the last remaining py.path-only code path in pytest, i.e. the last piece holding back py.path deprecation. The problem with it is that plugins/users use it from both sides -- implementing it (returning the value) and using it (using the return value). Dealing with implementers is easy enough -- allow to return `os.PathLike[str]`. But for callers who expect strictly `py.path` this will break and there's not really a good way to provide backward compat for this. From analyzing a corpus of 680 pytest plugins, the vast majority of `reportinfo` appearances are implementations, and the few callers don't actually access the path part of the return tuple. As for test suites that might access `reportinfo` (e.g. using `request.node.reportinfo()` or other ways), that is much harder to survey, but from the ones I searched, I only found case (`pytest_teamcity`, but even then it uses `str(fspath)` so is unlikely to be affected in practice). They are better served with using `node.location` or `node.path` directly. Therefore, just break it and change the return type to `str|os.PathLike[str]`. Refs #7259.
1 parent e84ba80 commit 7eee5c1

File tree

8 files changed

+28
-27
lines changed

8 files changed

+28
-27
lines changed

changelog/7259.breaking.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
2+
3+
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
4+
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
5+
6+
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
7+
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
8+
Note: pytest was not able to provide a deprecation period for this change.

doc/en/example/nonpython/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def repr_failure(self, excinfo):
4040
)
4141

4242
def reportinfo(self):
43-
return self.fspath, 0, f"usecase: {self.name}"
43+
return self.path, 0, f"usecase: {self.name}"
4444

4545

4646
class YamlException(Exception):

src/_pytest/doctest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Discover and run doctests in modules and test files."""
22
import bdb
33
import inspect
4+
import os
45
import platform
56
import sys
67
import traceback
@@ -28,7 +29,6 @@
2829
from _pytest._code.code import ReprFileLocation
2930
from _pytest._code.code import TerminalRepr
3031
from _pytest._io import TerminalWriter
31-
from _pytest.compat import legacy_path
3232
from _pytest.compat import safe_getattr
3333
from _pytest.config import Config
3434
from _pytest.config.argparsing import Parser
@@ -371,9 +371,9 @@ def repr_failure( # type: ignore[override]
371371
reprlocation_lines.append((reprlocation, lines))
372372
return ReprFailDoctest(reprlocation_lines)
373373

374-
def reportinfo(self):
374+
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
375375
assert self.dtest is not None
376-
return legacy_path(self.path), self.dtest.lineno, "[doctest] %s" % self.name
376+
return self.path, self.dtest.lineno, "[doctest] %s" % self.name
377377

378378

379379
def _get_flag_lookup() -> Dict[str, int]:

src/_pytest/nodes.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -718,15 +718,13 @@ def add_report_section(self, when: str, key: str, content: str) -> None:
718718
if content:
719719
self._report_sections.append((when, key, content))
720720

721-
def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], Optional[int], str]:
722-
723-
# TODO: enable Path objects in reportinfo
724-
return legacy_path(self.path), None, ""
721+
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
722+
return self.path, None, ""
725723

726724
@cached_property
727725
def location(self) -> Tuple[str, Optional[int], str]:
728726
location = self.reportinfo()
729-
fspath = absolutepath(str(location[0]))
730-
relfspath = self.session._node_location_to_relpath(fspath)
727+
path = absolutepath(os.fspath(location[0]))
728+
relfspath = self.session._node_location_to_relpath(path)
731729
assert type(location[2]) is str
732730
return (relfspath, location[1], location[2])

src/_pytest/python.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from _pytest.compat import is_async_function
4949
from _pytest.compat import is_generator
5050
from _pytest.compat import LEGACY_PATH
51-
from _pytest.compat import legacy_path
5251
from _pytest.compat import NOTSET
5352
from _pytest.compat import safe_getattr
5453
from _pytest.compat import safe_isclass
@@ -321,7 +320,7 @@ def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) ->
321320
parts.reverse()
322321
return ".".join(parts)
323322

324-
def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], int, str]:
323+
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
325324
# XXX caching?
326325
obj = self.obj
327326
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
@@ -330,17 +329,13 @@ def reportinfo(self) -> Tuple[Union[LEGACY_PATH, str], int, str]:
330329
file_path = sys.modules[obj.__module__].__file__
331330
if file_path.endswith(".pyc"):
332331
file_path = file_path[:-1]
333-
fspath: Union[LEGACY_PATH, str] = file_path
332+
path: Union["os.PathLike[str]", str] = file_path
334333
lineno = compat_co_firstlineno
335334
else:
336335
path, lineno = getfslineno(obj)
337-
if isinstance(path, Path):
338-
fspath = legacy_path(path)
339-
else:
340-
fspath = path
341336
modpath = self.getmodpath()
342337
assert isinstance(lineno, int)
343-
return fspath, lineno, modpath
338+
return path, lineno, modpath
344339

345340

346341
# As an optimization, these builtin attribute names are pre-ignored when

src/_pytest/reports.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,9 +324,9 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
324324
outcome = "skipped"
325325
r = excinfo._getreprcrash()
326326
if excinfo.value._use_item_location:
327-
filename, line = item.reportinfo()[:2]
327+
path, line = item.reportinfo()[:2]
328328
assert line is not None
329-
longrepr = str(filename), line + 1, r.message
329+
longrepr = os.fspath(path), line + 1, r.message
330330
else:
331331
longrepr = (str(r.path), r.lineno, r.message)
332332
else:

testing/python/collect.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,8 +1154,8 @@ def pytest_pycollect_makeitem(collector, name, obj):
11541154

11551155
def test_func_reportinfo(self, pytester: Pytester) -> None:
11561156
item = pytester.getitem("def test_func(): pass")
1157-
fspath, lineno, modpath = item.reportinfo()
1158-
assert str(fspath) == str(item.path)
1157+
path, lineno, modpath = item.reportinfo()
1158+
assert os.fspath(path) == str(item.path)
11591159
assert lineno == 0
11601160
assert modpath == "test_func"
11611161

@@ -1169,8 +1169,8 @@ def test_hello(self): pass
11691169
)
11701170
classcol = pytester.collect_by_name(modcol, "TestClass")
11711171
assert isinstance(classcol, Class)
1172-
fspath, lineno, msg = classcol.reportinfo()
1173-
assert str(fspath) == str(modcol.path)
1172+
path, lineno, msg = classcol.reportinfo()
1173+
assert os.fspath(path) == str(modcol.path)
11741174
assert lineno == 1
11751175
assert msg == "TestClass"
11761176

@@ -1194,7 +1194,7 @@ def intest_foo(self):
11941194
assert isinstance(classcol, Class)
11951195
instance = list(classcol.collect())[0]
11961196
assert isinstance(instance, Instance)
1197-
fspath, lineno, msg = instance.reportinfo()
1197+
path, lineno, msg = instance.reportinfo()
11981198

11991199

12001200
def test_customized_python_discovery(pytester: Pytester) -> None:

testing/python/integration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
1919
return MyCollector.from_parent(collector, name=name)
2020
class MyCollector(pytest.Collector):
2121
def reportinfo(self):
22-
return self.fspath, 3, "xyz"
22+
return self.path, 3, "xyz"
2323
"""
2424
)
2525
modcol = pytester.getmodulecol(
@@ -52,7 +52,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
5252
return MyCollector.from_parent(collector, name=name)
5353
class MyCollector(pytest.Collector):
5454
def reportinfo(self):
55-
return self.fspath, 3, "xyz"
55+
return self.path, 3, "xyz"
5656
"""
5757
)
5858
modcol = pytester.getmodulecol(

0 commit comments

Comments
 (0)