Skip to content

Commit

Permalink
Merge pull request pytest-dev#8122 from bluetech/py-to-pathlib-3
Browse files Browse the repository at this point in the history
Some py.path.local -> pathlib.Path
  • Loading branch information
bluetech committed Dec 13, 2020
2 parents 0958204 + ed658d6 commit 37b154b
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 126 deletions.
70 changes: 36 additions & 34 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@
from _pytest.compat import importlib_metadata
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import resolve_package_path
from _pytest.store import Store
from _pytest.warning_types import PytestConfigWarning

Expand Down Expand Up @@ -102,9 +104,7 @@ class ExitCode(enum.IntEnum):

class ConftestImportFailure(Exception):
def __init__(
self,
path: py.path.local,
excinfo: Tuple[Type[Exception], Exception, TracebackType],
self, path: Path, excinfo: Tuple[Type[Exception], Exception, TracebackType],
) -> None:
super().__init__(path, excinfo)
self.path = path
Expand Down Expand Up @@ -342,9 +342,9 @@ def __init__(self) -> None:
self._conftest_plugins: Set[types.ModuleType] = set()

# State related to local conftest plugins.
self._dirpath2confmods: Dict[py.path.local, List[types.ModuleType]] = {}
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
self._confcutdir: Optional[py.path.local] = None
self._confcutdir: Optional[Path] = None
self._noconftest = False
self._duplicatepaths: Set[py.path.local] = set()

Expand Down Expand Up @@ -479,9 +479,9 @@ def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
All builtin and 3rd party plugins will have been loaded, however, so
common options will not confuse our logic here.
"""
current = py.path.local()
current = Path.cwd()
self._confcutdir = (
current.join(namespace.confcutdir, abs=True)
absolutepath(current / namespace.confcutdir)
if namespace.confcutdir
else None
)
Expand All @@ -495,51 +495,51 @@ def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
i = path.find("::")
if i != -1:
path = path[:i]
anchor = current.join(path, abs=1)
anchor = absolutepath(current / path)
if anchor.exists(): # we found some file object
self._try_load_conftest(anchor, namespace.importmode)
foundanchor = True
if not foundanchor:
self._try_load_conftest(current, namespace.importmode)

def _try_load_conftest(
self, anchor: py.path.local, importmode: Union[str, ImportMode]
self, anchor: Path, importmode: Union[str, ImportMode]
) -> None:
self._getconftestmodules(anchor, importmode)
# let's also consider test* subdirs
if anchor.check(dir=1):
for x in anchor.listdir("test*"):
if x.check(dir=1):
if anchor.is_dir():
for x in anchor.glob("test*"):
if x.is_dir():
self._getconftestmodules(x, importmode)

@lru_cache(maxsize=128)
def _getconftestmodules(
self, path: py.path.local, importmode: Union[str, ImportMode],
self, path: Path, importmode: Union[str, ImportMode],
) -> List[types.ModuleType]:
if self._noconftest:
return []

if path.isfile():
directory = path.dirpath()
if path.is_file():
directory = path.parent
else:
directory = path

# XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir.
clist = []
for parent in directory.parts():
if self._confcutdir and self._confcutdir.relto(parent):
for parent in reversed((directory, *directory.parents)):
if self._confcutdir and parent in self._confcutdir.parents:
continue
conftestpath = parent.join("conftest.py")
if conftestpath.isfile():
conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(conftestpath, importmode)
clist.append(mod)
self._dirpath2confmods[directory] = clist
return clist

def _rget_with_confmod(
self, name: str, path: py.path.local, importmode: Union[str, ImportMode],
self, name: str, path: Path, importmode: Union[str, ImportMode],
) -> Tuple[types.ModuleType, Any]:
modules = self._getconftestmodules(path, importmode)
for mod in reversed(modules):
Expand All @@ -550,21 +550,21 @@ def _rget_with_confmod(
raise KeyError(name)

def _importconftest(
self, conftestpath: py.path.local, importmode: Union[str, ImportMode],
self, conftestpath: Path, importmode: Union[str, ImportMode],
) -> types.ModuleType:
# Use a resolved Path object as key to avoid loading the same conftest
# twice with build systems that create build directories containing
# symlinks to actual files.
# Using Path().resolve() is better than py.path.realpath because
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
key = Path(str(conftestpath)).resolve()
key = conftestpath.resolve()

with contextlib.suppress(KeyError):
return self._conftestpath2mod[key]

pkgpath = conftestpath.pypkgpath()
pkgpath = resolve_package_path(conftestpath)
if pkgpath is None:
_ensure_removed_sysmodule(conftestpath.purebasename)
_ensure_removed_sysmodule(conftestpath.stem)

try:
mod = import_path(conftestpath, mode=importmode)
Expand All @@ -577,18 +577,18 @@ def _importconftest(

self._conftest_plugins.add(mod)
self._conftestpath2mod[key] = mod
dirpath = conftestpath.dirpath()
dirpath = conftestpath.parent
if dirpath in self._dirpath2confmods:
for path, mods in self._dirpath2confmods.items():
if path and path.relto(dirpath) or path == dirpath:
if path and dirpath in path.parents or path == dirpath:
assert mod not in mods
mods.append(mod)
self.trace(f"loading conftestmodule {mod!r}")
self.consider_conftest(mod)
return mod

def _check_non_top_pytest_plugins(
self, mod: types.ModuleType, conftestpath: py.path.local,
self, mod: types.ModuleType, conftestpath: Path,
) -> None:
if (
hasattr(mod, "pytest_plugins")
Expand Down Expand Up @@ -1412,21 +1412,23 @@ def _getini(self, name: str):
assert type in [None, "string"]
return value

def _getconftest_pathlist(
self, name: str, path: py.path.local
) -> Optional[List[py.path.local]]:
def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
try:
mod, relroots = self.pluginmanager._rget_with_confmod(
name, path, self.getoption("importmode")
)
except KeyError:
return None
modpath = py.path.local(mod.__file__).dirpath()
values: List[py.path.local] = []
modpath = Path(mod.__file__).parent
values: List[Path] = []
for relroot in relroots:
if not isinstance(relroot, py.path.local):
if isinstance(relroot, Path):
pass
elif isinstance(relroot, py.path.local):
relroot = Path(relroot)
else:
relroot = relroot.replace("/", os.sep)
relroot = modpath.join(relroot, abs=True)
relroot = absolutepath(modpath / relroot)
values.append(relroot)
return values

Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import types
import warnings
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
Expand Down Expand Up @@ -525,7 +526,7 @@ def _find(

if self.fspath.basename == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.fspath, self.config.getoption("importmode")
Path(self.fspath), self.config.getoption("importmode")
)
else:
try:
Expand Down
18 changes: 10 additions & 8 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,22 +371,23 @@ def _in_venv(path: py.path.local) -> bool:


def pytest_ignore_collect(path: py.path.local, config: Config) -> Optional[bool]:
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
path_ = Path(path)
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path_.parent)
ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore")
if excludeopt:
ignore_paths.extend([py.path.local(x) for x in excludeopt])
ignore_paths.extend(absolutepath(x) for x in excludeopt)

if py.path.local(path) in ignore_paths:
if path_ in ignore_paths:
return True

ignore_globs = config._getconftest_pathlist(
"collect_ignore_glob", path=path.dirpath()
"collect_ignore_glob", path=path_.parent
)
ignore_globs = ignore_globs or []
excludeglobopt = config.getoption("ignore_glob")
if excludeglobopt:
ignore_globs.extend([py.path.local(x) for x in excludeglobopt])
ignore_globs.extend(absolutepath(x) for x in excludeglobopt)

if any(fnmatch.fnmatch(str(path), str(glob)) for glob in ignore_globs):
return True
Expand Down Expand Up @@ -512,12 +513,12 @@ def pytest_runtest_logreport(
def isinitpath(self, path: py.path.local) -> bool:
return path in self._initialpaths

def gethookproxy(self, fspath: py.path.local):
def gethookproxy(self, fspath: "os.PathLike[str]"):
# Check if we have the common case of running
# hooks with all conftest.py files.
pm = self.config.pluginmanager
my_conftestmodules = pm._getconftestmodules(
fspath, self.config.getoption("importmode")
Path(fspath), self.config.getoption("importmode")
)
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods:
Expand Down Expand Up @@ -668,8 +669,9 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
# No point in finding packages when collecting doctests.
if not self.config.getoption("doctestmodules", False):
pm = self.config.pluginmanager
confcutdir = py.path.local(pm._confcutdir) if pm._confcutdir else None
for parent in reversed(argpath.parts()):
if pm._confcutdir and pm._confcutdir.relto(parent):
if confcutdir and confcutdir.relto(parent):
break

if parent.isdir():
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ def from_parent(cls, parent, *, fspath, **kw):
"""The public constructor."""
return super().from_parent(parent=parent, fspath=fspath, **kw)

def gethookproxy(self, fspath: py.path.local):
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)

Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ def setup(self) -> None:
func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func)

def gethookproxy(self, fspath: py.path.local):
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)

Expand Down
12 changes: 7 additions & 5 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from _pytest.config import ExitCode
from _pytest.fixtures import FixtureRequest
from _pytest.pytester import get_public_names
from _pytest.pytester import Pytester
from _pytest.pytester import Testdir


Expand Down Expand Up @@ -1961,8 +1962,10 @@ def test_result(arg):
reprec = testdir.inline_run("-v", "-s")
reprec.assertoutcome(passed=4)

def test_class_function_parametrization_finalization(self, testdir):
p = testdir.makeconftest(
def test_class_function_parametrization_finalization(
self, pytester: Pytester
) -> None:
p = pytester.makeconftest(
"""
import pytest
import pprint
Expand All @@ -1984,7 +1987,7 @@ def fin():
request.addfinalizer(fin)
"""
)
testdir.makepyfile(
pytester.makepyfile(
"""
import pytest
Expand All @@ -1996,8 +1999,7 @@ def test_2(self):
pass
"""
)
confcut = f"--confcutdir={testdir.tmpdir}"
reprec = testdir.inline_run("-v", "-s", confcut)
reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path)
reprec.assertoutcome(passed=8)
config = reprec.getcalls("pytest_unconfigure")[0].config
values = config.pluginmanager._getconftestmodules(p, importmode="prepend")[
Expand Down
4 changes: 3 additions & 1 deletion testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@ def pytest_ignore_collect(path, config):
def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None:
pytester.makeconftest(
"""
collect_ignore = ['hello', 'test_world.py']
import py
from pathlib import Path
collect_ignore = [py.path.local('hello'), 'test_world.py', Path('bye')]
def pytest_addoption(parser):
parser.addoption("--XX", action="store_true", default=False)
def pytest_configure(config):
Expand Down
18 changes: 9 additions & 9 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,16 +574,16 @@ def test_getoption(self, pytester: Pytester) -> None:
config.getvalue("x")
assert config.getoption("x", 1) == 1

def test_getconftest_pathlist(self, pytester: Pytester, tmpdir) -> None:
somepath = tmpdir.join("x", "y", "z")
p = tmpdir.join("conftest.py")
p.write("pathlist = ['.', %r]" % str(somepath))
def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
somepath = tmp_path.joinpath("x", "y", "z")
p = tmp_path.joinpath("conftest.py")
p.write_text(f"pathlist = ['.', {str(somepath)!r}]")
config = pytester.parseconfigure(p)
assert config._getconftest_pathlist("notexist", path=tmpdir) is None
pl = config._getconftest_pathlist("pathlist", path=tmpdir) or []
assert config._getconftest_pathlist("notexist", path=tmp_path) is None
pl = config._getconftest_pathlist("pathlist", path=tmp_path) or []
print(pl)
assert len(pl) == 2
assert pl[0] == tmpdir
assert pl[0] == tmp_path
assert pl[1] == somepath

@pytest.mark.parametrize("maybe_type", ["not passed", "None", '"string"'])
Expand Down Expand Up @@ -1935,10 +1935,10 @@ def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives
assert msg not in res.stdout.str()


def test_conftest_import_error_repr(tmpdir: py.path.local) -> None:
def test_conftest_import_error_repr(tmp_path: Path) -> None:
"""`ConftestImportFailure` should use a short error message and readable
path to the failed conftest.py file."""
path = tmpdir.join("foo/conftest.py")
path = tmp_path.joinpath("foo/conftest.py")
with pytest.raises(
ConftestImportFailure,
match=re.escape(f"RuntimeError: some error (from {path})"),
Expand Down

0 comments on commit 37b154b

Please sign in to comment.