Merge pull request #824 from nicoddemus/drop-py-path

Replace py.path.local usages by pathlib.Path
This commit is contained in:
Bruno Oliveira
2022-10-22 07:00:42 -03:00
committed by GitHub
10 changed files with 121 additions and 73 deletions

View File

@@ -1,4 +1,9 @@
repos: repos:
- repo: https://github.com/PyCQA/autoflake
rev: v1.7.6
hooks:
- id: autoflake
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.3.0 rev: 22.3.0
hooks: hooks:

View File

@@ -0,0 +1 @@
Replace internal usages of ``py.path.local`` by ``pathlib.Path``.

View File

@@ -17,7 +17,8 @@ Example:
import pytest import pytest
@pytest.mark.parametrize("param", {"a","b"})
@pytest.mark.parametrize("param", {"a", "b"})
def test_pytest_parametrize_unordered(param): def test_pytest_parametrize_unordered(param):
pass pass
@@ -37,6 +38,7 @@ Some solutions:
import pytest import pytest
@pytest.mark.parametrize("param", ["a", "b"]) @pytest.mark.parametrize("param", ["a", "b"])
def test_pytest_parametrize_unordered(param): def test_pytest_parametrize_unordered(param):
pass pass
@@ -47,6 +49,7 @@ Some solutions:
import pytest import pytest
@pytest.mark.parametrize("param", sorted({"a", "b"})) @pytest.mark.parametrize("param", sorted({"a", "b"}))
def test_pytest_parametrize_unordered(param): def test_pytest_parametrize_unordered(param):
pass pass
@@ -54,7 +57,7 @@ Some solutions:
Output (stdout and stderr) from workers Output (stdout and stderr) from workers
--------------------------------------- ---------------------------------------
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. 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.
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. 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.

19
src/xdist/_path.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from itertools import chain
from pathlib import Path
from typing import Callable, Iterator
def visit_path(
path: Path, *, filter: Callable[[Path], bool], recurse: Callable[[Path], bool]
) -> Iterator[Path]:
"""
Implements the interface of ``py.path.local.visit()`` for Path objects,
to simplify porting the code over from ``py.path.local``.
"""
for dirpath, dirnames, filenames in os.walk(path):
dirnames[:] = [x for x in dirnames if recurse(Path(dirpath, x))]
for name in chain(dirnames, filenames):
p = Path(dirpath, name)
if filter(p):
yield p

View File

@@ -6,11 +6,17 @@
processes) otherwise changes to source code can crash processes) otherwise changes to source code can crash
the controlling process which should best never happen. the controlling process which should best never happen.
""" """
import py import os
from pathlib import Path
from typing import Dict, Sequence
import pytest import pytest
import sys import sys
import time import time
import execnet import execnet
from _pytest._io import TerminalWriter
from xdist._path import visit_path
@pytest.hookimpl @pytest.hookimpl
@@ -38,9 +44,9 @@ def pytest_cmdline_main(config):
return 2 # looponfail only can get stop with ctrl-C anyway return 2 # looponfail only can get stop with ctrl-C anyway
def looponfail_main(config): def looponfail_main(config: pytest.Config) -> None:
remotecontrol = RemoteControl(config) remotecontrol = RemoteControl(config)
rootdirs = [py.path.local(root) for root in config.getini("looponfailroots")] rootdirs = [Path(root) for root in config.getini("looponfailroots")]
statrecorder = StatRecorder(rootdirs) statrecorder = StatRecorder(rootdirs)
try: try:
while 1: while 1:
@@ -71,7 +77,7 @@ class RemoteControl:
def setup(self, out=None): def setup(self, out=None):
if out is None: if out is None:
out = py.io.TerminalWriter() out = TerminalWriter()
if hasattr(self, "gateway"): if hasattr(self, "gateway"):
raise ValueError("already have gateway %r" % self.gateway) raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up worker session") self.trace("setting up worker session")
@@ -129,7 +135,7 @@ class RemoteControl:
def repr_pytest_looponfailinfo(failreports, rootdirs): def repr_pytest_looponfailinfo(failreports, rootdirs):
tr = py.io.TerminalWriter() tr = TerminalWriter()
if failreports: if failreports:
tr.sep("#", "LOOPONFAILING", bold=True) tr.sep("#", "LOOPONFAILING", bold=True)
for report in failreports: for report in failreports:
@@ -225,16 +231,16 @@ class WorkerFailSession:
class StatRecorder: class StatRecorder:
def __init__(self, rootdirlist): def __init__(self, rootdirlist: Sequence[Path]) -> None:
self.rootdirlist = rootdirlist self.rootdirlist = rootdirlist
self.statcache = {} self.statcache: Dict[Path, os.stat_result] = {}
self.check() # snapshot state self.check() # snapshot state
def fil(self, p): def fil(self, p: Path) -> bool:
return p.check(file=1, dotfile=0) and p.ext != ".pyc" return p.is_file() and not p.name.startswith(".") and p.suffix != ".pyc"
def rec(self, p): def rec(self, p: Path) -> bool:
return p.check(dotfile=0) return not p.name.startswith(".") and p.exists()
def waitonchange(self, checkinterval=1.0): def waitonchange(self, checkinterval=1.0):
while 1: while 1:
@@ -243,34 +249,34 @@ class StatRecorder:
return return
time.sleep(checkinterval) time.sleep(checkinterval)
def check(self, removepycfiles=True): # noqa, too complex def check(self, removepycfiles: bool = True) -> bool: # noqa, too complex
changed = False changed = False
statcache = self.statcache newstat: Dict[Path, os.stat_result] = {}
newstat = {}
for rootdir in self.rootdirlist: for rootdir in self.rootdirlist:
for path in rootdir.visit(self.fil, self.rec): for path in visit_path(rootdir, filter=self.fil, recurse=self.rec):
oldstat = statcache.pop(path, None) oldstat = self.statcache.pop(path, None)
try: try:
newstat[path] = curstat = path.stat() curstat = path.stat()
except py.error.ENOENT: except OSError:
if oldstat: if oldstat:
changed = True changed = True
else: else:
if oldstat: newstat[path] = curstat
if oldstat is not None:
if ( if (
oldstat.mtime != curstat.mtime oldstat.st_mtime != curstat.st_mtime
or oldstat.size != curstat.size or oldstat.st_size != curstat.st_size
): ):
changed = True changed = True
print("# MODIFIED", path) print("# MODIFIED", path)
if removepycfiles and path.ext == ".py": if removepycfiles and path.suffix == ".py":
pycfile = path + "c" pycfile = path.with_suffix(".pyc")
if pycfile.check(): if pycfile.is_file():
pycfile.remove() os.unlink(pycfile)
else: else:
changed = True changed = True
if statcache: if self.statcache:
changed = True changed = True
self.statcache = newstat self.statcache = newstat
return changed return changed

View File

@@ -3,7 +3,6 @@ import uuid
import sys import sys
from pathlib import Path from pathlib import Path
import py
import pytest import pytest
@@ -165,7 +164,7 @@ def pytest_addoption(parser):
"looponfailroots", "looponfailroots",
type="paths" if PYTEST_GTE_7 else "pathlist", type="paths" if PYTEST_GTE_7 else "pathlist",
help="directories to check for changes", help="directories to check for changes",
default=[Path.cwd() if PYTEST_GTE_7 else py.path.local()], default=[Path.cwd()],
) )

View File

@@ -3,6 +3,8 @@ import os
import re import re
import sys import sys
import uuid import uuid
from pathlib import Path
from typing import List, Union, Sequence, Optional, Any, Tuple, Set
import py import py
import pytest import pytest
@@ -33,7 +35,7 @@ class NodeManager:
EXIT_TIMEOUT = 10 EXIT_TIMEOUT = 10
DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"] DEFAULT_IGNORES = [".*", "*.pyc", "*.pyo", "*~"]
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): def __init__(self, config, specs=None, defaultchdir="pyexecnetcache") -> None:
self.config = config self.config = config
self.trace = self.config.trace.get("nodemanager") self.trace = self.config.trace.get("nodemanager")
self.testrunuid = self.config.getoption("testrunuid") self.testrunuid = self.config.getoption("testrunuid")
@@ -52,7 +54,7 @@ class NodeManager:
self.specs.append(spec) self.specs.append(spec)
self.roots = self._getrsyncdirs() self.roots = self._getrsyncdirs()
self.rsyncoptions = self._getrsyncoptions() self.rsyncoptions = self._getrsyncoptions()
self._rsynced_specs = set() self._rsynced_specs: Set[Tuple[Any, Any]] = set()
def rsync_roots(self, gateway): def rsync_roots(self, gateway):
"""Rsync the set of roots to the node's gateway cwd.""" """Rsync the set of roots to the node's gateway cwd."""
@@ -81,7 +83,7 @@ class NodeManager:
def _getxspecs(self): def _getxspecs(self):
return [execnet.XSpec(x) for x in parse_spec_config(self.config)] return [execnet.XSpec(x) for x in parse_spec_config(self.config)]
def _getrsyncdirs(self): def _getrsyncdirs(self) -> List[Path]:
for spec in self.specs: for spec in self.specs:
if not spec.popen or spec.chdir: if not spec.popen or spec.chdir:
break break
@@ -108,8 +110,8 @@ class NodeManager:
candidates.extend(rsyncroots) candidates.extend(rsyncroots)
roots = [] roots = []
for root in candidates: for root in candidates:
root = py.path.local(root).realpath() root = Path(root).resolve()
if not root.check(): if not root.exists():
raise pytest.UsageError("rsyncdir doesn't exist: {!r}".format(root)) raise pytest.UsageError("rsyncdir doesn't exist: {!r}".format(root))
if root not in roots: if root not in roots:
roots.append(root) roots.append(root)
@@ -160,18 +162,24 @@ class NodeManager:
class HostRSync(execnet.RSync): class HostRSync(execnet.RSync):
"""RSyncer that filters out common files""" """RSyncer that filters out common files"""
def __init__(self, sourcedir, *args, **kwargs): PathLike = Union[str, "os.PathLike[str]"]
self._synced = {}
ignores = kwargs.pop("ignores", None) or []
self._ignores = [
re.compile(fnmatch.translate(getattr(x, "strpath", x))) for x in ignores
]
super().__init__(sourcedir=sourcedir, **kwargs)
def filter(self, path): def __init__(
path = py.path.local(path) self,
sourcedir: PathLike,
*,
ignores: Optional[Sequence[PathLike]] = None,
**kwargs: object
) -> None:
if ignores is None:
ignores = []
self._ignores = [re.compile(fnmatch.translate(os.fspath(x))) for x in ignores]
super().__init__(sourcedir=Path(sourcedir), **kwargs)
def filter(self, path: PathLike) -> bool:
path = Path(path)
for cre in self._ignores: for cre in self._ignores:
if cre.match(path.basename) or cre.match(path.strpath): if cre.match(path.name) or cre.match(str(path)):
return False return False
else: else:
return True return True
@@ -187,20 +195,28 @@ class HostRSync(execnet.RSync):
print("{}:{} <= {}".format(gateway.spec, remotepath, path)) print("{}:{} <= {}".format(gateway.spec, remotepath, path))
def make_reltoroot(roots, args): def make_reltoroot(roots: Sequence[Path], args: List[str]) -> List[str]:
# XXX introduce/use public API for splitting pytest args # XXX introduce/use public API for splitting pytest args
splitcode = "::" splitcode = "::"
result = [] result = []
for arg in args: for arg in args:
parts = arg.split(splitcode) parts = arg.split(splitcode)
fspath = py.path.local(parts[0]) fspath = Path(parts[0])
if not fspath.exists(): try:
exists = fspath.exists()
except OSError:
exists = False
if not exists:
result.append(arg) result.append(arg)
continue continue
for root in roots: for root in roots:
x = fspath.relto(root) x: Optional[Path]
try:
x = fspath.relative_to(root)
except ValueError:
x = None
if x or fspath == root: if x or fspath == root:
parts[0] = root.basename + "/" + x parts[0] = root.name + "/" + str(x)
break break
else: else:
raise ValueError("arg {} not relative to an rsync root".format(arg)) raise ValueError("arg {} not relative to an rsync root".format(arg))

View File

@@ -1,4 +1,6 @@
import py import unittest.mock
from typing import List
import pytest import pytest
import shutil import shutil
import textwrap import textwrap
@@ -16,7 +18,7 @@ class TestStatRecorder:
tmp = tmp_path tmp = tmp_path
hello = tmp / "hello.py" hello = tmp / "hello.py"
hello.touch() hello.touch()
sd = StatRecorder([py.path.local(tmp)]) sd = StatRecorder([tmp])
changed = sd.check() changed = sd.check()
assert not changed assert not changed
@@ -56,15 +58,12 @@ class TestStatRecorder:
tmp = tmp_path tmp = tmp_path
tmp.joinpath("dir").mkdir() tmp.joinpath("dir").mkdir()
tmp.joinpath("dir", "hello.py").touch() tmp.joinpath("dir", "hello.py").touch()
sd = StatRecorder([py.path.local(tmp)]) sd = StatRecorder([tmp])
assert not sd.fil(py.path.local(tmp / "dir")) assert not sd.fil(tmp / "dir")
def test_filechange_deletion_race( def test_filechange_deletion_race(self, tmp_path: Path) -> None:
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
tmp = tmp_path tmp = tmp_path
pytmp = py.path.local(tmp) sd = StatRecorder([tmp])
sd = StatRecorder([pytmp])
changed = sd.check() changed = sd.check()
assert not changed assert not changed
@@ -76,16 +75,20 @@ class TestStatRecorder:
p.unlink() p.unlink()
# make check()'s visit() call return our just removed # make check()'s visit() call return our just removed
# path as if we were in a race condition # path as if we were in a race condition
monkeypatch.setattr(pytmp, "visit", lambda *args: [py.path.local(p)]) dirname = str(tmp)
dirnames: List[str] = []
changed = sd.check() filenames = [str(p)]
with unittest.mock.patch(
"os.walk", return_value=[(dirname, dirnames, filenames)], autospec=True
):
changed = sd.check()
assert changed assert changed
def test_pycremoval(self, tmp_path: Path) -> None: def test_pycremoval(self, tmp_path: Path) -> None:
tmp = tmp_path tmp = tmp_path
hello = tmp / "hello.py" hello = tmp / "hello.py"
hello.touch() hello.touch()
sd = StatRecorder([py.path.local(tmp)]) sd = StatRecorder([tmp])
changed = sd.check() changed = sd.check()
assert not changed assert not changed
@@ -100,7 +103,7 @@ class TestStatRecorder:
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
tmp = tmp_path tmp = tmp_path
sd = StatRecorder([py.path.local(tmp)]) sd = StatRecorder([tmp])
ret_values = [True, False] ret_values = [True, False]
monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop()) monkeypatch.setattr(StatRecorder, "check", lambda self: ret_values.pop())

View File

@@ -1,5 +1,4 @@
import pprint import pprint
import py
import pytest import pytest
import sys import sys
import uuid import uuid
@@ -108,7 +107,7 @@ class TestWorkerInteractor:
assert ev.name == "collectionstart" assert ev.name == "collectionstart"
assert not ev.kwargs assert not ev.kwargs
ev = worker.popevent("collectionfinish") ev = worker.popevent("collectionfinish")
assert ev.kwargs["topdir"] == py.path.local(worker.pytester.path) assert ev.kwargs["topdir"] == str(worker.pytester.path)
ids = ev.kwargs["ids"] ids = ev.kwargs["ids"]
assert len(ids) == 1 assert len(ids) == 1
worker.sendcommand("runtests", indices=list(range(len(ids)))) worker.sendcommand("runtests", indices=list(range(len(ids))))

View File

@@ -1,5 +1,4 @@
import execnet import execnet
import py
import pytest import pytest
import shutil import shutil
import textwrap import textwrap
@@ -7,6 +6,7 @@ import warnings
from pathlib import Path from pathlib import Path
from util import generate_warning from util import generate_warning
from xdist import workermanage from xdist import workermanage
from xdist._path import visit_path
from xdist.remote import serialize_warning_message from xdist.remote import serialize_warning_message
from xdist.workermanage import HostRSync, NodeManager, unserialize_warning_message from xdist.workermanage import HostRSync, NodeManager, unserialize_warning_message
@@ -157,12 +157,9 @@ class TestHRSync:
source.joinpath("somedir").mkdir() source.joinpath("somedir").mkdir()
source.joinpath("somedir", "editfile~").touch() source.joinpath("somedir", "editfile~").touch()
syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES) syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES)
files = list(py.path.local(source).visit(rec=syncer.filter, fil=syncer.filter)) files = list(visit_path(source, recurse=syncer.filter, filter=syncer.filter))
assert len(files) == 3 names = {x.name for x in files}
basenames = [x.basename for x in files] assert names == {"dir", "file.txt", "somedir"}
assert "dir" in basenames
assert "file.txt" in basenames
assert "somedir" in basenames
def test_hrsync_one_host(self, source: Path, dest: Path) -> None: def test_hrsync_one_host(self, source: Path, dest: Path) -> None:
gw = execnet.makegateway("popen//chdir=%s" % dest) gw = execnet.makegateway("popen//chdir=%s" % dest)