Skip to content

Commit 436932e

Browse files
[wip] implement Node.path as pathlib.Path
there is a bug in module collection left that i dont see tonight, lets retry tommorow
1 parent 325d701 commit 436932e

File tree

15 files changed

+167
-76
lines changed

15 files changed

+167
-76
lines changed

src/_pytest/cacheprovider.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,12 @@ def pytest_make_collect_report(self, collector: nodes.Collector):
220220
lf_paths = self.lfplugin._last_failed_paths
221221
res.result = sorted(
222222
res.result,
223-
key=lambda x: 0 if Path(str(x.fspath)) in lf_paths else 1,
223+
key=lambda x: x.path not in lf_paths,
224224
)
225225
return
226226

227227
elif isinstance(collector, Module):
228-
if Path(str(collector.fspath)) in self.lfplugin._last_failed_paths:
228+
if collector.path in self.lfplugin._last_failed_paths:
229229
out = yield
230230
res = out.get_result()
231231
result = res.result
@@ -246,7 +246,7 @@ def pytest_make_collect_report(self, collector: nodes.Collector):
246246
for x in result
247247
if x.nodeid in lastfailed
248248
# Include any passed arguments (not trivial to filter).
249-
or session.isinitpath(x.fspath)
249+
or session.isinitpath(x.path)
250250
# Keep all sub-collectors.
251251
or isinstance(x, nodes.Collector)
252252
]
@@ -266,7 +266,7 @@ def pytest_make_collect_report(
266266
# test-bearing paths and doesn't try to include the paths of their
267267
# packages, so don't filter them.
268268
if isinstance(collector, Module) and not isinstance(collector, Package):
269-
if Path(str(collector.fspath)) not in self.lfplugin._last_failed_paths:
269+
if collector.path not in self.lfplugin._last_failed_paths:
270270
self.lfplugin._skipped_files += 1
271271

272272
return CollectReport(
@@ -415,7 +415,7 @@ def pytest_collection_modifyitems(
415415
self.cached_nodeids.update(item.nodeid for item in items)
416416

417417
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
418-
return sorted(items, key=lambda item: item.fspath.mtime(), reverse=True)
418+
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
419419

420420
def pytest_sessionfinish(self) -> None:
421421
config = self.config

src/_pytest/compat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import enum
33
import functools
44
import inspect
5+
import os
56
import re
67
import sys
78
from contextlib import contextmanager
@@ -18,6 +19,7 @@
1819
from typing import Union
1920

2021
import attr
22+
import py
2123

2224
from _pytest.outcomes import fail
2325
from _pytest.outcomes import TEST_OUTCOME
@@ -30,6 +32,16 @@
3032
_T = TypeVar("_T")
3133
_S = TypeVar("_S")
3234

35+
#: constant to prepare valuing py.path.local replacements/lazy proxies later on
36+
# intended for removal in pytest 8.0 or 9.0
37+
38+
LEGACY_PATH = py.path.local
39+
40+
41+
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
42+
"""Internal wrapper to prepare lazy proxies for py.path.local instances"""
43+
return py.path.local(path)
44+
3345

3446
# fmt: off
3547
# Singleton type for NOTSET, as described in:

src/_pytest/deprecated.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@
8989
)
9090

9191

92+
NODE_FSPATH = UnformattedWarning(
93+
PytestDeprecationWarning,
94+
"{type}.fspath is deprecated and will be replaced by {type}.path.\n"
95+
"see TODO;URL for details on replacing py.path.local with pathlib.Path",
96+
)
97+
9298
# You want to make some `__init__` or function "private".
9399
#
94100
# def my_private_function(some, args):
@@ -106,6 +112,8 @@
106112
#
107113
# All other calls will get the default _ispytest=False and trigger
108114
# the warning (possibly error in the future).
115+
116+
109117
def check_ispytest(ispytest: bool) -> None:
110118
if not ispytest:
111119
warn(PRIVATE, stacklevel=3)

src/_pytest/doctest.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from _pytest._code.code import ReprFileLocation
3131
from _pytest._code.code import TerminalRepr
3232
from _pytest._io import TerminalWriter
33+
from _pytest.compat import legacy_path
3334
from _pytest.compat import safe_getattr
3435
from _pytest.config import Config
3536
from _pytest.config.argparsing import Parser
@@ -128,10 +129,10 @@ def pytest_collect_file(
128129
config = parent.config
129130
if fspath.suffix == ".py":
130131
if config.option.doctestmodules and not _is_setup_py(fspath):
131-
mod: DoctestModule = DoctestModule.from_parent(parent, fspath=path)
132+
mod: DoctestModule = DoctestModule.from_parent(parent, path=fspath)
132133
return mod
133134
elif _is_doctest(config, fspath, parent):
134-
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, fspath=path)
135+
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=fspath)
135136
return txt
136137
return None
137138

@@ -378,7 +379,7 @@ def repr_failure( # type: ignore[override]
378379

379380
def reportinfo(self):
380381
assert self.dtest is not None
381-
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
382+
return legacy_path(self.path), self.dtest.lineno, "[doctest] %s" % self.name
382383

383384

384385
def _get_flag_lookup() -> Dict[str, int]:
@@ -425,9 +426,9 @@ def collect(self) -> Iterable[DoctestItem]:
425426
# Inspired by doctest.testfile; ideally we would use it directly,
426427
# but it doesn't support passing a custom checker.
427428
encoding = self.config.getini("doctest_encoding")
428-
text = self.fspath.read_text(encoding)
429-
filename = str(self.fspath)
430-
name = self.fspath.basename
429+
text = self.path.read_text(encoding)
430+
filename = str(self.path)
431+
name = self.path.name
431432
globs = {"__name__": "__main__"}
432433

433434
optionflags = get_optionflags(self)
@@ -534,16 +535,16 @@ def _find(
534535
self, tests, obj, name, module, source_lines, globs, seen
535536
)
536537

537-
if self.fspath.basename == "conftest.py":
538+
if self.path.name == "conftest.py":
538539
module = self.config.pluginmanager._importconftest(
539-
Path(self.fspath), self.config.getoption("importmode")
540+
self.path, self.config.getoption("importmode")
540541
)
541542
else:
542543
try:
543-
module = import_path(self.fspath)
544+
module = import_path(self.path)
544545
except ImportError:
545546
if self.config.getvalue("doctest_ignore_import_errors"):
546-
pytest.skip("unable to import module %r" % self.fspath)
547+
pytest.skip("unable to import module %r" % self.path)
547548
else:
548549
raise
549550
# Uses internal doctest module parsing mechanism.

src/_pytest/fixtures.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
from typing import Union
2929

3030
import attr
31-
import py
3231

3332
import _pytest
3433
from _pytest import nodes
@@ -46,13 +45,16 @@
4645
from _pytest.compat import getimfunc
4746
from _pytest.compat import getlocation
4847
from _pytest.compat import is_generator
48+
from _pytest.compat import LEGACY_PATH
49+
from _pytest.compat import legacy_path
4950
from _pytest.compat import NOTSET
5051
from _pytest.compat import safe_getattr
5152
from _pytest.config import _PluggyPlugin
5253
from _pytest.config import Config
5354
from _pytest.config.argparsing import Parser
5455
from _pytest.deprecated import check_ispytest
5556
from _pytest.deprecated import FILLFUNCARGS
57+
from _pytest.deprecated import NODE_FSPATH
5658
from _pytest.deprecated import YIELD_FIXTURE
5759
from _pytest.mark import Mark
5860
from _pytest.mark import ParameterSet
@@ -256,12 +258,12 @@ def get_parametrized_fixture_keys(item: nodes.Item, scopenum: int) -> Iterator[_
256258
if scopenum == 0: # session
257259
key: _Key = (argname, param_index)
258260
elif scopenum == 1: # package
259-
key = (argname, param_index, item.fspath.dirpath())
261+
key = (argname, param_index, item.path.parent)
260262
elif scopenum == 2: # module
261-
key = (argname, param_index, item.fspath)
263+
key = (argname, param_index, item.path)
262264
elif scopenum == 3: # class
263265
item_cls = item.cls # type: ignore[attr-defined]
264-
key = (argname, param_index, item.fspath, item_cls)
266+
key = (argname, param_index, item.path, item_cls)
265267
yield key
266268

267269

@@ -519,12 +521,17 @@ def module(self):
519521
return self._pyfuncitem.getparent(_pytest.python.Module).obj
520522

521523
@property
522-
def fspath(self) -> py.path.local:
523-
"""The file system path of the test module which collected this test."""
524+
def fspath(self) -> LEGACY_PATH:
525+
"""(deprecated) The file system path of the test module which collected this test."""
526+
warnings.warn(NODE_FSPATH.format(type=type(self).__name__), stacklevel=2)
527+
return legacy_path(self.path)
528+
529+
@property
530+
def path(self) -> Path:
524531
if self.scope not in ("function", "class", "module", "package"):
525532
raise AttributeError(f"module not available in {self.scope}-scoped context")
526533
# TODO: Remove ignore once _pyfuncitem is properly typed.
527-
return self._pyfuncitem.fspath # type: ignore
534+
return self._pyfuncitem.path # type: ignore
528535

529536
@property
530537
def keywords(self) -> MutableMapping[str, Any]:
@@ -1040,7 +1047,7 @@ def finish(self, request: SubRequest) -> None:
10401047
if exc:
10411048
raise exc
10421049
finally:
1043-
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
1050+
hook = self._fixturemanager.session.gethookproxy(request.node.path)
10441051
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
10451052
# Even if finalization fails, we invalidate the cached fixture
10461053
# value and remove all finalizers because they may be bound methods
@@ -1075,7 +1082,7 @@ def execute(self, request: SubRequest) -> _FixtureValue:
10751082
self.finish(request)
10761083
assert self.cached_result is None
10771084

1078-
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
1085+
hook = self._fixturemanager.session.gethookproxy(request.node.path)
10791086
result = hook.pytest_fixture_setup(fixturedef=self, request=request)
10801087
return result
10811088

src/_pytest/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,12 @@ class Session(nodes.FSCollector):
464464

465465
def __init__(self, config: Config) -> None:
466466
super().__init__(
467-
config.rootdir, parent=None, config=config, session=self, nodeid=""
467+
path=config.rootpath,
468+
fspath=config.rootdir,
469+
parent=None,
470+
config=config,
471+
session=self,
472+
nodeid="",
468473
)
469474
self.testsfailed = 0
470475
self.testscollected = 0
@@ -688,7 +693,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
688693
if col:
689694
if isinstance(col[0], Package):
690695
pkg_roots[str(parent)] = col[0]
691-
node_cache1[Path(col[0].fspath)] = [col[0]]
696+
node_cache1[col[0].path] = [col[0]]
692697

693698
# If it's a directory argument, recurse and look for any Subpackages.
694699
# Let the Package collector deal with subnodes, don't collect here.
@@ -717,7 +722,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
717722
continue
718723

719724
for x in self._collectfile(path):
720-
key2 = (type(x), Path(x.fspath))
725+
key2 = (type(x), x.path)
721726
if key2 in node_cache2:
722727
yield node_cache2[key2]
723728
else:

0 commit comments

Comments
 (0)