Skip to content

Commit a1785b5

Browse files
authored
Merge pull request #824 from nicoddemus/drop-py-path
Replace py.path.local usages by pathlib.Path
2 parents f04cd22 + 25cc1a4 commit a1785b5

10 files changed

+122
-74
lines changed

.pre-commit-config.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
repos:
2+
- repo: https://github.com/PyCQA/autoflake
3+
rev: v1.7.6
4+
hooks:
5+
- id: autoflake
6+
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
27
- repo: https://github.com/psf/black
38
rev: 22.3.0
49
hooks:

changelog/824.trivial.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace internal usages of ``py.path.local`` by ``pathlib.Path``.

docs/known-limitations.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Example:
1717
1818
import pytest
1919
20-
@pytest.mark.parametrize("param", {"a","b"})
20+
21+
@pytest.mark.parametrize("param", {"a", "b"})
2122
def test_pytest_parametrize_unordered(param):
2223
pass
2324
@@ -37,6 +38,7 @@ Some solutions:
3738
3839
import pytest
3940
41+
4042
@pytest.mark.parametrize("param", ["a", "b"])
4143
def test_pytest_parametrize_unordered(param):
4244
pass
@@ -47,14 +49,15 @@ Some solutions:
4749
4850
import pytest
4951
52+
5053
@pytest.mark.parametrize("param", sorted({"a", "b"}))
5154
def test_pytest_parametrize_unordered(param):
5255
pass
5356
5457
Output (stdout and stderr) from workers
5558
---------------------------------------
5659

57-
The ``-s``/``--capture=no`` option is meant to disable pytest capture, so users can then see stdout and stderr output in the terminal from tests and application code in real time.
60+
The ``-s``/``--capture=no`` option is meant to disable pytest capture, so users can then see stdout and stderr output in the terminal from tests and application code in real time.
5861

5962
However this option does not work with ``pytest-xdist`` because `execnet <https://github.com/pytest-dev/execnet>`__ the underlying library used for communication between master and workers, does not support transferring stdout/stderr from workers.
6063

src/xdist/_path.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
from itertools import chain
3+
from pathlib import Path
4+
from typing import Callable, Iterator
5+
6+
7+
def visit_path(
8+
path: Path, *, filter: Callable[[Path], bool], recurse: Callable[[Path], bool]
9+
) -> Iterator[Path]:
10+
"""
11+
Implements the interface of ``py.path.local.visit()`` for Path objects,
12+
to simplify porting the code over from ``py.path.local``.
13+
"""
14+
for dirpath, dirnames, filenames in os.walk(path):
15+
dirnames[:] = [x for x in dirnames if recurse(Path(dirpath, x))]
16+
for name in chain(dirnames, filenames):
17+
p = Path(dirpath, name)
18+
if filter(p):
19+
yield p

src/xdist/looponfail.py

+32-26
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
processes) otherwise changes to source code can crash
77
the controlling process which should best never happen.
88
"""
9-
import py
9+
import os
10+
from pathlib import Path
11+
from typing import Dict, Sequence
12+
1013
import pytest
1114
import sys
1215
import time
1316
import execnet
17+
from _pytest._io import TerminalWriter
18+
19+
from xdist._path import visit_path
1420

1521

1622
@pytest.hookimpl
@@ -38,9 +44,9 @@ def pytest_cmdline_main(config):
3844
return 2 # looponfail only can get stop with ctrl-C anyway
3945

4046

41-
def looponfail_main(config):
47+
def looponfail_main(config: pytest.Config) -> None:
4248
remotecontrol = RemoteControl(config)
43-
rootdirs = [py.path.local(root) for root in config.getini("looponfailroots")]
49+
rootdirs = [Path(root) for root in config.getini("looponfailroots")]
4450
statrecorder = StatRecorder(rootdirs)
4551
try:
4652
while 1:
@@ -71,7 +77,7 @@ def initgateway(self):
7177

7278
def setup(self, out=None):
7379
if out is None:
74-
out = py.io.TerminalWriter()
80+
out = TerminalWriter()
7581
if hasattr(self, "gateway"):
7682
raise ValueError("already have gateway %r" % self.gateway)
7783
self.trace("setting up worker session")
@@ -129,7 +135,7 @@ def loop_once(self):
129135

130136

131137
def repr_pytest_looponfailinfo(failreports, rootdirs):
132-
tr = py.io.TerminalWriter()
138+
tr = TerminalWriter()
133139
if failreports:
134140
tr.sep("#", "LOOPONFAILING", bold=True)
135141
for report in failreports:
@@ -225,16 +231,16 @@ def main(self):
225231

226232

227233
class StatRecorder:
228-
def __init__(self, rootdirlist):
234+
def __init__(self, rootdirlist: Sequence[Path]) -> None:
229235
self.rootdirlist = rootdirlist
230-
self.statcache = {}
236+
self.statcache: Dict[Path, os.stat_result] = {}
231237
self.check() # snapshot state
232238

233-
def fil(self, p):
234-
return p.check(file=1, dotfile=0) and p.ext != ".pyc"
239+
def fil(self, p: Path) -> bool:
240+
return p.is_file() and not p.name.startswith(".") and p.suffix != ".pyc"
235241

236-
def rec(self, p):
237-
return p.check(dotfile=0)
242+
def rec(self, p: Path) -> bool:
243+
return not p.name.startswith(".") and p.exists()
238244

239245
def waitonchange(self, checkinterval=1.0):
240246
while 1:
@@ -243,34 +249,34 @@ def waitonchange(self, checkinterval=1.0):
243249
return
244250
time.sleep(checkinterval)
245251

246-
def check(self, removepycfiles=True): # noqa, too complex
252+
def check(self, removepycfiles: bool = True) -> bool: # noqa, too complex
247253
changed = False
248-
statcache = self.statcache
249-
newstat = {}
254+
newstat: Dict[Path, os.stat_result] = {}
250255
for rootdir in self.rootdirlist:
251-
for path in rootdir.visit(self.fil, self.rec):
252-
oldstat = statcache.pop(path, None)
256+
for path in visit_path(rootdir, filter=self.fil, recurse=self.rec):
257+
oldstat = self.statcache.pop(path, None)
253258
try:
254-
newstat[path] = curstat = path.stat()
255-
except py.error.ENOENT:
259+
curstat = path.stat()
260+
except OSError:
256261
if oldstat:
257262
changed = True
258263
else:
259-
if oldstat:
264+
newstat[path] = curstat
265+
if oldstat is not None:
260266
if (
261-
oldstat.mtime != curstat.mtime
262-
or oldstat.size != curstat.size
267+
oldstat.st_mtime != curstat.st_mtime
268+
or oldstat.st_size != curstat.st_size
263269
):
264270
changed = True
265271
print("# MODIFIED", path)
266-
if removepycfiles and path.ext == ".py":
267-
pycfile = path + "c"
268-
if pycfile.check():
269-
pycfile.remove()
272+
if removepycfiles and path.suffix == ".py":
273+
pycfile = path.with_suffix(".pyc")
274+
if pycfile.is_file():
275+
os.unlink(pycfile)
270276

271277
else:
272278
changed = True
273-
if statcache:
279+
if self.statcache:
274280
changed = True
275281
self.statcache = newstat
276282
return changed

src/xdist/plugin.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import sys
44
from pathlib import Path
55

6-
import py
76
import pytest
87

98

@@ -165,7 +164,7 @@ def pytest_addoption(parser):
165164
"looponfailroots",
166165
type="paths" if PYTEST_GTE_7 else "pathlist",
167166
help="directories to check for changes",
168-
default=[Path.cwd() if PYTEST_GTE_7 else py.path.local()],
167+
default=[Path.cwd()],
169168
)
170169

171170

src/xdist/workermanage.py

+37-21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import re
44
import sys
55
import uuid
6+
from pathlib import Path
7+
from typing import List, Union, Sequence, Optional, Any, Tuple, Set
68

79
import py
810
import pytest
@@ -33,7 +35,7 @@ class NodeManager:
3335
EXIT_TIMEOUT = 10
3436
DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"]
3537

36-
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
38+
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache") -> None:
3739
self.config = config
3840
self.trace = self.config.trace.get("nodemanager")
3941
self.testrunuid = self.config.getoption("testrunuid")
@@ -52,7 +54,7 @@ def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
5254
self.specs.append(spec)
5355
self.roots = self._getrsyncdirs()
5456
self.rsyncoptions = self._getrsyncoptions()
55-
self._rsynced_specs = set()
57+
self._rsynced_specs: Set[Tuple[Any, Any]] = set()
5658

5759
def rsync_roots(self, gateway):
5860
"""Rsync the set of roots to the node's gateway cwd."""
@@ -81,7 +83,7 @@ def teardown_nodes(self):
8183
def _getxspecs(self):
8284
return [execnet.XSpec(x) for x in parse_spec_config(self.config)]
8385

84-
def _getrsyncdirs(self):
86+
def _getrsyncdirs(self) -> List[Path]:
8587
for spec in self.specs:
8688
if not spec.popen or spec.chdir:
8789
break
@@ -108,8 +110,8 @@ def get_dir(p):
108110
candidates.extend(rsyncroots)
109111
roots = []
110112
for root in candidates:
111-
root = py.path.local(root).realpath()
112-
if not root.check():
113+
root = Path(root).resolve()
114+
if not root.exists():
113115
raise pytest.UsageError("rsyncdir doesn't exist: {!r}".format(root))
114116
if root not in roots:
115117
roots.append(root)
@@ -160,18 +162,24 @@ def finished():
160162
class HostRSync(execnet.RSync):
161163
"""RSyncer that filters out common files"""
162164

163-
def __init__(self, sourcedir, *args, **kwargs):
164-
self._synced = {}
165-
ignores = kwargs.pop("ignores", None) or []
166-
self._ignores = [
167-
re.compile(fnmatch.translate(getattr(x, "strpath", x))) for x in ignores
168-
]
169-
super().__init__(sourcedir=sourcedir, **kwargs)
170-
171-
def filter(self, path):
172-
path = py.path.local(path)
165+
PathLike = Union[str, "os.PathLike[str]"]
166+
167+
def __init__(
168+
self,
169+
sourcedir: PathLike,
170+
*,
171+
ignores: Optional[Sequence[PathLike]] = None,
172+
**kwargs: object
173+
) -> None:
174+
if ignores is None:
175+
ignores = []
176+
self._ignores = [re.compile(fnmatch.translate(os.fspath(x))) for x in ignores]
177+
super().__init__(sourcedir=Path(sourcedir), **kwargs)
178+
179+
def filter(self, path: PathLike) -> bool:
180+
path = Path(path)
173181
for cre in self._ignores:
174-
if cre.match(path.basename) or cre.match(path.strpath):
182+
if cre.match(path.name) or cre.match(str(path)):
175183
return False
176184
else:
177185
return True
@@ -187,20 +195,28 @@ def _report_send_file(self, gateway, modified_rel_path):
187195
print("{}:{} <= {}".format(gateway.spec, remotepath, path))
188196

189197

190-
def make_reltoroot(roots, args):
198+
def make_reltoroot(roots: Sequence[Path], args: List[str]) -> List[str]:
191199
# XXX introduce/use public API for splitting pytest args
192200
splitcode = "::"
193201
result = []
194202
for arg in args:
195203
parts = arg.split(splitcode)
196-
fspath = py.path.local(parts[0])
197-
if not fspath.exists():
204+
fspath = Path(parts[0])
205+
try:
206+
exists = fspath.exists()
207+
except OSError:
208+
exists = False
209+
if not exists:
198210
result.append(arg)
199211
continue
200212
for root in roots:
201-
x = fspath.relto(root)
213+
x: Optional[Path]
214+
try:
215+
x = fspath.relative_to(root)
216+
except ValueError:
217+
x = None
202218
if x or fspath == root:
203-
parts[0] = root.basename + "/" + x
219+
parts[0] = root.name + "/" + str(x)
204220
break
205221
else:
206222
raise ValueError("arg {} not relative to an rsync root".format(arg))

testing/test_looponfail.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import py
1+
import unittest.mock
2+
from typing import List
3+
24
import pytest
35
import shutil
46
import textwrap
@@ -16,7 +18,7 @@ def test_filechange(self, tmp_path: Path) -> None:
1618
tmp = tmp_path
1719
hello = tmp / "hello.py"
1820
hello.touch()
19-
sd = StatRecorder([py.path.local(tmp)])
21+
sd = StatRecorder([tmp])
2022
changed = sd.check()
2123
assert not changed
2224

@@ -56,15 +58,12 @@ def test_dirchange(self, tmp_path: Path) -> None:
5658
tmp = tmp_path
5759
tmp.joinpath("dir").mkdir()
5860
tmp.joinpath("dir", "hello.py").touch()
59-
sd = StatRecorder([py.path.local(tmp)])
60-
assert not sd.fil(py.path.local(tmp / "dir"))
61+
sd = StatRecorder([tmp])
62+
assert not sd.fil(tmp / "dir")
6163

62-
def test_filechange_deletion_race(
63-
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
64-
) -> None:
64+
def test_filechange_deletion_race(self, tmp_path: Path) -> None:
6565
tmp = tmp_path
66-
pytmp = py.path.local(tmp)
67-
sd = StatRecorder([pytmp])
66+
sd = StatRecorder([tmp])
6867
changed = sd.check()
6968
assert not changed
7069

@@ -76,16 +75,20 @@ def test_filechange_deletion_race(
7675
p.unlink()
7776
# make check()'s visit() call return our just removed
7877
# path as if we were in a race condition
79-
monkeypatch.setattr(pytmp, "visit", lambda *args: [py.path.local(p)])
80-
81-
changed = sd.check()
78+
dirname = str(tmp)
79+
dirnames: List[str] = []
80+
filenames = [str(p)]
81+
with unittest.mock.patch(
82+
"os.walk", return_value=[(dirname, dirnames, filenames)], autospec=True
83+
):
84+
changed = sd.check()
8285
assert changed
8386

8487
def test_pycremoval(self, tmp_path: Path) -> None:
8588
tmp = tmp_path
8689
hello = tmp / "hello.py"
8790
hello.touch()
88-
sd = StatRecorder([py.path.local(tmp)])
91+
sd = StatRecorder([tmp])
8992
changed = sd.check()
9093
assert not changed
9194

@@ -100,7 +103,7 @@ def test_waitonchange(
100103
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
101104
) -> None:
102105
tmp = tmp_path
103-
sd = StatRecorder([py.path.local(tmp)])
106+
sd = StatRecorder([tmp])
104107

105108
ret_values = [True, False]
106109
monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop())

0 commit comments

Comments
 (0)