Skip to content

Commit 35df3e6

Browse files
authored
Merge pull request #8459 from bluetech/unnecessary-py-path-3
2 parents fb481c7 + 4690e4c commit 35df3e6

11 files changed

+88
-41
lines changed

src/_pytest/fixtures.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -670,7 +670,7 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
670670
"\n\nRequested here:\n{}:{}".format(
671671
funcitem.nodeid,
672672
fixturedef.argname,
673-
getlocation(fixturedef.func, funcitem.config.rootdir),
673+
getlocation(fixturedef.func, funcitem.config.rootpath),
674674
source_path_str,
675675
source_lineno,
676676
)
@@ -728,7 +728,7 @@ def _factorytraceback(self) -> List[str]:
728728
fs, lineno = getfslineno(factory)
729729
if isinstance(fs, Path):
730730
session: Session = self._pyfuncitem.session
731-
p = bestrelpath(Path(session.fspath), fs)
731+
p = bestrelpath(session.path, fs)
732732
else:
733733
p = fs
734734
args = _format_args(factory)

src/_pytest/main.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import _pytest._code
2626
from _pytest import nodes
2727
from _pytest.compat import final
28+
from _pytest.compat import LEGACY_PATH
2829
from _pytest.compat import legacy_path
2930
from _pytest.config import Config
3031
from _pytest.config import directory_arg
@@ -301,7 +302,7 @@ def wrap_session(
301302
finally:
302303
# Explicitly break reference cycle.
303304
excinfo = None # type: ignore
304-
session.startdir.chdir()
305+
os.chdir(session.startpath)
305306
if initstate >= 2:
306307
try:
307308
config.hook.pytest_sessionfinish(
@@ -476,7 +477,6 @@ def __init__(self, config: Config) -> None:
476477
self.shouldstop: Union[bool, str] = False
477478
self.shouldfail: Union[bool, str] = False
478479
self.trace = config.trace.root.get("collection")
479-
self.startdir = config.invocation_dir
480480
self._initialpaths: FrozenSet[Path] = frozenset()
481481

482482
self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath)
@@ -497,6 +497,24 @@ def __repr__(self) -> str:
497497
self.testscollected,
498498
)
499499

500+
@property
501+
def startpath(self) -> Path:
502+
"""The path from which pytest was invoked.
503+
504+
.. versionadded:: 6.3.0
505+
"""
506+
return self.config.invocation_params.dir
507+
508+
@property
509+
def stardir(self) -> LEGACY_PATH:
510+
"""The path from which pytest was invoked.
511+
512+
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
513+
514+
:type: LEGACY_PATH
515+
"""
516+
return legacy_path(self.startpath)
517+
500518
def _node_location_to_relpath(self, node_path: Path) -> str:
501519
# bestrelpath is a quite slow function.
502520
return self._bestrelpathcache[node_path]

src/_pytest/nodes.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from _pytest.mark.structures import NodeKeywords
3333
from _pytest.outcomes import fail
3434
from _pytest.pathlib import absolutepath
35+
from _pytest.pathlib import commonpath
3536
from _pytest.store import Store
3637

3738
if TYPE_CHECKING:
@@ -517,13 +518,11 @@ def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
517518
excinfo.traceback = ntraceback.filter()
518519

519520

520-
def _check_initialpaths_for_relpath(
521-
session: "Session", fspath: LEGACY_PATH
522-
) -> Optional[str]:
521+
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
523522
for initial_path in session._initialpaths:
524-
initial_path_ = legacy_path(initial_path)
525-
if fspath.common(initial_path_) == initial_path_:
526-
return fspath.relto(initial_path_)
523+
if commonpath(path, initial_path) == initial_path:
524+
rel = str(path.relative_to(initial_path))
525+
return "" if rel == "." else rel
527526
return None
528527

529528

@@ -538,7 +537,7 @@ def __init__(
538537
nodeid: Optional[str] = None,
539538
) -> None:
540539
path, fspath = _imply_path(path, fspath=fspath)
541-
name = fspath.basename
540+
name = path.name
542541
if parent is not None and parent.path != path:
543542
try:
544543
rel = path.relative_to(parent.path)
@@ -547,15 +546,15 @@ def __init__(
547546
else:
548547
name = str(rel)
549548
name = name.replace(os.sep, SEP)
550-
self.path = Path(fspath)
549+
self.path = path
551550

552551
session = session or parent.session
553552

554553
if nodeid is None:
555554
try:
556555
nodeid = str(self.path.relative_to(session.config.rootpath))
557556
except ValueError:
558-
nodeid = _check_initialpaths_for_relpath(session, fspath)
557+
nodeid = _check_initialpaths_for_relpath(session, path)
559558

560559
if nodeid and os.sep != SEP:
561560
nodeid = nodeid.replace(os.sep, SEP)

src/_pytest/pathlib.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
583583

584584

585585
def visit(
586-
path: str, recurse: Callable[["os.DirEntry[str]"], bool]
586+
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
587587
) -> Iterator["os.DirEntry[str]"]:
588588
"""Walk a directory recursively, in breadth-first order.
589589
@@ -657,3 +657,21 @@ def bestrelpath(directory: Path, dest: Path) -> str:
657657
# Forward from base to dest.
658658
*reldest.parts,
659659
)
660+
661+
662+
# Originates from py. path.local.copy(), with siginficant trims and adjustments.
663+
# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True)
664+
def copytree(source: Path, target: Path) -> None:
665+
"""Recursively copy a source directory to target."""
666+
assert source.is_dir()
667+
for entry in visit(source, recurse=lambda entry: not entry.is_symlink()):
668+
x = Path(entry)
669+
relpath = x.relative_to(source)
670+
newx = target / relpath
671+
newx.parent.mkdir(exist_ok=True)
672+
if x.is_symlink():
673+
newx.symlink_to(os.readlink(x))
674+
elif x.is_file():
675+
shutil.copyfile(x, newx)
676+
elif x.is_dir():
677+
newx.mkdir(exist_ok=True)

src/_pytest/pytester.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from _pytest.outcomes import importorskip
6464
from _pytest.outcomes import skip
6565
from _pytest.pathlib import bestrelpath
66+
from _pytest.pathlib import copytree
6667
from _pytest.pathlib import make_numbered_dir
6768
from _pytest.reports import CollectReport
6869
from _pytest.reports import TestReport
@@ -935,10 +936,7 @@ def copy_example(self, name: Optional[str] = None) -> Path:
935936
example_path = example_dir.joinpath(name)
936937

937938
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
938-
# TODO: legacy_path.copy can copy files to existing directories,
939-
# while with shutil.copytree the destination directory cannot exist,
940-
# we will need to roll our own in order to drop legacy_path completely
941-
legacy_path(example_path).copy(legacy_path(self.path))
939+
copytree(example_path, self.path)
942940
return self.path
943941
elif example_path.is_file():
944942
result = self.path.joinpath(example_path.name)

src/_pytest/python.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ def __init__(
645645
session=session,
646646
nodeid=nodeid,
647647
)
648-
self.name = os.path.basename(str(fspath.dirname))
648+
self.name = path.parent.name
649649

650650
def setup(self) -> None:
651651
# Not using fixtures to call setup_module here because autouse fixtures

src/_pytest/reports.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import os
12
from io import StringIO
2-
from pathlib import Path
33
from pprint import pprint
44
from typing import Any
55
from typing import cast
@@ -29,7 +29,6 @@
2929
from _pytest._code.code import TerminalRepr
3030
from _pytest._io import TerminalWriter
3131
from _pytest.compat import final
32-
from _pytest.compat import LEGACY_PATH
3332
from _pytest.config import Config
3433
from _pytest.nodes import Collector
3534
from _pytest.nodes import Item
@@ -500,8 +499,8 @@ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]:
500499
else:
501500
d["longrepr"] = report.longrepr
502501
for name in d:
503-
if isinstance(d[name], (LEGACY_PATH, Path)):
504-
d[name] = str(d[name])
502+
if isinstance(d[name], os.PathLike):
503+
d[name] = os.fspath(d[name])
505504
elif name == "result":
506505
d[name] = None # for now
507506
return d

src/_pytest/terminal.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
from _pytest._code.code import ExceptionRepr
3838
from _pytest._io.wcwidth import wcswidth
3939
from _pytest.compat import final
40+
from _pytest.compat import LEGACY_PATH
41+
from _pytest.compat import legacy_path
4042
from _pytest.config import _PluggyPlugin
4143
from _pytest.config import Config
4244
from _pytest.config import ExitCode
@@ -318,7 +320,6 @@ def __init__(self, config: Config, file: Optional[TextIO] = None) -> None:
318320
self.stats: Dict[str, List[Any]] = {}
319321
self._main_color: Optional[str] = None
320322
self._known_types: Optional[List[str]] = None
321-
self.startdir = config.invocation_dir
322323
self.startpath = config.invocation_params.dir
323324
if file is None:
324325
file = sys.stdout
@@ -381,6 +382,16 @@ def showfspath(self, value: Optional[bool]) -> None:
381382
def showlongtestinfo(self) -> bool:
382383
return self.verbosity > 0
383384

385+
@property
386+
def startdir(self) -> LEGACY_PATH:
387+
"""The directory from which pytest was invoked.
388+
389+
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
390+
391+
:type: LEGACY_PATH
392+
"""
393+
return legacy_path(self.startpath)
394+
384395
def hasopt(self, char: str) -> bool:
385396
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
386397
return char in self.reportchars

testing/python/collect.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -933,11 +933,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
933933
"""\
934934
import pytest
935935
def pytest_runtest_setup(item):
936-
assert item.fspath.purebasename == "test_in_sub1"
936+
assert item.path.stem == "test_in_sub1"
937937
def pytest_runtest_call(item):
938-
assert item.fspath.purebasename == "test_in_sub1"
938+
assert item.path.stem == "test_in_sub1"
939939
def pytest_runtest_teardown(item):
940-
assert item.fspath.purebasename == "test_in_sub1"
940+
assert item.path.stem == "test_in_sub1"
941941
"""
942942
)
943943
)
@@ -946,11 +946,11 @@ def pytest_runtest_teardown(item):
946946
"""\
947947
import pytest
948948
def pytest_runtest_setup(item):
949-
assert item.fspath.purebasename == "test_in_sub2"
949+
assert item.path.stem == "test_in_sub2"
950950
def pytest_runtest_call(item):
951-
assert item.fspath.purebasename == "test_in_sub2"
951+
assert item.path.stem == "test_in_sub2"
952952
def pytest_runtest_teardown(item):
953-
assert item.fspath.purebasename == "test_in_sub2"
953+
assert item.path.stem == "test_in_sub2"
954954
"""
955955
)
956956
)
@@ -1125,8 +1125,7 @@ def pytest_pycollect_makeitem(collector, name, obj):
11251125
def test_func_reportinfo(self, pytester: Pytester) -> None:
11261126
item = pytester.getitem("def test_func(): pass")
11271127
fspath, lineno, modpath = item.reportinfo()
1128-
with pytest.warns(DeprecationWarning):
1129-
assert fspath == item.fspath
1128+
assert str(fspath) == str(item.path)
11301129
assert lineno == 0
11311130
assert modpath == "test_func"
11321131

@@ -1141,8 +1140,7 @@ def test_hello(self): pass
11411140
classcol = pytester.collect_by_name(modcol, "TestClass")
11421141
assert isinstance(classcol, Class)
11431142
fspath, lineno, msg = classcol.reportinfo()
1144-
with pytest.warns(DeprecationWarning):
1145-
assert fspath == modcol.fspath
1143+
assert str(fspath) == str(modcol.path)
11461144
assert lineno == 1
11471145
assert msg == "TestClass"
11481146

testing/test_nodes.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import pytest
77
from _pytest import nodes
8-
from _pytest.compat import legacy_path
98
from _pytest.pytester import Pytester
109
from _pytest.warning_types import PytestWarning
1110

@@ -76,7 +75,7 @@ class FakeSession1:
7675

7776
session = cast(pytest.Session, FakeSession1)
7877

79-
assert nodes._check_initialpaths_for_relpath(session, legacy_path(cwd)) == ""
78+
assert nodes._check_initialpaths_for_relpath(session, cwd) == ""
8079

8180
sub = cwd / "file"
8281

@@ -85,9 +84,9 @@ class FakeSession2:
8584

8685
session = cast(pytest.Session, FakeSession2)
8786

88-
assert nodes._check_initialpaths_for_relpath(session, legacy_path(sub)) == "file"
87+
assert nodes._check_initialpaths_for_relpath(session, sub) == "file"
8988

90-
outside = legacy_path("/outside")
89+
outside = Path("/outside")
9190
assert nodes._check_initialpaths_for_relpath(session, outside) is None
9291

9392

testing/test_reports.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import pytest
55
from _pytest._code.code import ExceptionChainRepr
66
from _pytest._code.code import ExceptionRepr
7-
from _pytest.compat import legacy_path
87
from _pytest.config import Config
98
from _pytest.pytester import Pytester
109
from _pytest.reports import CollectReport
@@ -225,18 +224,26 @@ def test_extended_report_deserialization(self, pytester: Pytester) -> None:
225224
assert newrep.longrepr == str(rep.longrepr)
226225

227226
def test_paths_support(self, pytester: Pytester) -> None:
228-
"""Report attributes which are py.path or pathlib objects should become strings."""
227+
"""Report attributes which are path-like should become strings."""
229228
pytester.makepyfile(
230229
"""
231230
def test_a():
232231
assert False
233232
"""
234233
)
234+
235+
class MyPathLike:
236+
def __init__(self, path: str) -> None:
237+
self.path = path
238+
239+
def __fspath__(self) -> str:
240+
return self.path
241+
235242
reprec = pytester.inline_run()
236243
reports = reprec.getreports("pytest_runtest_logreport")
237244
assert len(reports) == 3
238245
test_a_call = reports[1]
239-
test_a_call.path1 = legacy_path(pytester.path) # type: ignore[attr-defined]
246+
test_a_call.path1 = MyPathLike(str(pytester.path)) # type: ignore[attr-defined]
240247
test_a_call.path2 = pytester.path # type: ignore[attr-defined]
241248
data = test_a_call._to_json()
242249
assert data["path1"] == str(pytester.path)

0 commit comments

Comments
 (0)