From e0d0951945dd4d7a80b1324e4fd6ad59c3c334ad Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 4 Aug 2020 10:12:27 +0300 Subject: [PATCH 1/4] pathlib: add analogues to py.path.local's bestrelpath and common An equivalent for these py.path.local functions is needed for some upcoming py.path -> pathlib conversions. --- src/_pytest/pathlib.py | 32 ++++++++++++++++++++++++++++++++ testing/test_pathlib.py | 20 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index b3129020d..c3235f8b8 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -569,3 +569,35 @@ def visit( for entry in entries: if entry.is_dir(follow_symlinks=False) and recurse(entry): yield from visit(entry.path, recurse) + + +def commonpath(path1: Path, path2: Path) -> Optional[Path]: + """Return the common part shared with the other path, or None if there is + no common part.""" + try: + return Path(os.path.commonpath((str(path1), str(path2)))) + except ValueError: + return None + + +def bestrelpath(directory: Path, dest: Path) -> str: + """Return a string which is a relative path from directory to dest such + that directory/bestrelpath == dest. + + If no such path can be determined, returns dest. + """ + if dest == directory: + return os.curdir + # Find the longest common directory. + base = commonpath(directory, dest) + # Can be the case on Windows. + if not base: + return str(dest) + reldirectory = directory.relative_to(base) + reldest = dest.relative_to(base) + return os.path.join( + # Back from directory to base. + *([os.pardir] * len(reldirectory.parts)), + # Forward from base to dest. + *reldest.parts, + ) diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 74dac21d9..41228d6b0 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -6,6 +6,8 @@ from textwrap import dedent import py import pytest +from _pytest.pathlib import bestrelpath +from _pytest.pathlib import commonpath from _pytest.pathlib import ensure_deletable from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import get_extended_length_path_str @@ -381,3 +383,21 @@ def test_suppress_error_removing_lock(tmp_path): # check now that we can remove the lock file in normal circumstances assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30) assert not lock.is_file() + + +def test_bestrelpath() -> None: + curdir = Path("/foo/bar/baz/path") + assert bestrelpath(curdir, curdir) == "." + assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world" + assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister" + assert bestrelpath(curdir, curdir.parent) == ".." + assert bestrelpath(curdir, Path("hello")) == "hello" + + +def test_commonpath() -> None: + path = Path("/foo/bar/baz/path") + subpath = path / "sampledir" + assert commonpath(path, subpath) == path + assert commonpath(subpath, path) == path + assert commonpath(Path(str(path) + "suffix"), path) == path.parent + assert commonpath(path, path.parent.parent) == path.parent.parent From 9e55288ba498a3a7ba9a909b40e0b70a0c69f9b3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 5 Aug 2020 18:36:41 +0300 Subject: [PATCH 2/4] pathlib: add absolutepath() as alternative to Path.resolve() Didn't call it absolute or absolute_path to avoid conflicts with possible variable names. Didn't call it abspath to avoid confusion with os.path.abspath. --- src/_pytest/pathlib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index c3235f8b8..4a249c8fd 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -571,6 +571,15 @@ def visit( yield from visit(entry.path, recurse) +def absolutepath(path: Union[Path, str]) -> Path: + """Convert a path to an absolute path using os.path.abspath. + + Prefer this over Path.resolve() (see #6523). + Prefer this over Path.absolute() (not public, doesn't normalize). + """ + return Path(os.path.abspath(str(path))) + + def commonpath(path1: Path, path2: Path) -> Optional[Path]: """Return the common part shared with the other path, or None if there is no common part.""" From 70f3ad1c1f31b35d4004f92734b4afd6c8fbdecf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 3 Aug 2020 17:46:35 +0300 Subject: [PATCH 3/4] config/findpaths: convert from py.path.local to pathlib --- src/_pytest/config/__init__.py | 5 +- src/_pytest/config/findpaths.py | 98 +++++++++------- testing/test_config.py | 192 ++++++++++++++++++-------------- testing/test_findpaths.py | 98 ++++++++-------- 4 files changed, 222 insertions(+), 171 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 455a14b40..6305cdbd5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1006,12 +1006,15 @@ class Config: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) - self.rootdir, self.inifile, self.inicfg = determine_setup( + rootpath, inipath, inicfg = determine_setup( ns.inifilename, ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, config=self, ) + self.rootdir = py.path.local(str(rootpath)) + self.inifile = py.path.local(str(inipath)) if inipath else None + self.inicfg = inicfg self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile self._parser.addini("addopts", "extra command line options", "args") diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index be25fc829..65120e484 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,23 +1,28 @@ +import itertools import os +import sys from typing import Dict from typing import Iterable from typing import List from typing import Optional +from typing import Sequence from typing import Tuple from typing import Union import iniconfig -import py from .exceptions import UsageError from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath +from _pytest.pathlib import commonpath +from _pytest.pathlib import Path if TYPE_CHECKING: from . import Config -def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: +def _parse_ini_config(path: Path) -> iniconfig.IniConfig: """Parse the given generic '.ini' file using legacy IniConfig parser, returning the parsed object. @@ -30,7 +35,7 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: def load_config_dict_from_file( - filepath: py.path.local, + filepath: Path, ) -> Optional[Dict[str, Union[str, List[str]]]]: """Load pytest configuration from the given file path, if supported. @@ -38,18 +43,18 @@ def load_config_dict_from_file( """ # Configuration from ini files are obtained from the [pytest] section, if present. - if filepath.ext == ".ini": + if filepath.suffix == ".ini": iniconfig = _parse_ini_config(filepath) if "pytest" in iniconfig: return dict(iniconfig["pytest"].items()) else: # "pytest.ini" files are always the source of configuration, even if empty. - if filepath.basename == "pytest.ini": + if filepath.name == "pytest.ini": return {} # '.cfg' files are considered if they contain a "[tool:pytest]" section. - elif filepath.ext == ".cfg": + elif filepath.suffix == ".cfg": iniconfig = _parse_ini_config(filepath) if "tool:pytest" in iniconfig.sections: @@ -60,7 +65,7 @@ def load_config_dict_from_file( fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) # '.toml' files are considered if they contain a [tool.pytest.ini_options] table. - elif filepath.ext == ".toml": + elif filepath.suffix == ".toml": import toml config = toml.load(str(filepath)) @@ -79,9 +84,9 @@ def load_config_dict_from_file( def locate_config( - args: Iterable[Union[str, py.path.local]] + args: Iterable[Path], ) -> Tuple[ - Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], + Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]], ]: """Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict).""" @@ -93,62 +98,77 @@ def locate_config( ] args = [x for x in args if not str(x).startswith("-")] if not args: - args = [py.path.local()] + args = [Path.cwd()] for arg in args: - arg = py.path.local(arg) - for base in arg.parts(reverse=True): + argpath = absolutepath(arg) + for base in itertools.chain((argpath,), reversed(argpath.parents)): for config_name in config_names: - p = base.join(config_name) - if p.isfile(): + p = base / config_name + if p.is_file(): ini_config = load_config_dict_from_file(p) if ini_config is not None: return base, p, ini_config return None, None, {} -def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: - common_ancestor = None # type: Optional[py.path.local] +def get_common_ancestor(paths: Iterable[Path]) -> Path: + common_ancestor = None # type: Optional[Path] for path in paths: if not path.exists(): continue if common_ancestor is None: common_ancestor = path else: - if path.relto(common_ancestor) or path == common_ancestor: + if common_ancestor in path.parents or path == common_ancestor: continue - elif common_ancestor.relto(path): + elif path in common_ancestor.parents: common_ancestor = path else: - shared = path.common(common_ancestor) + shared = commonpath(path, common_ancestor) if shared is not None: common_ancestor = shared if common_ancestor is None: - common_ancestor = py.path.local() - elif common_ancestor.isfile(): - common_ancestor = common_ancestor.dirpath() + common_ancestor = Path.cwd() + elif common_ancestor.is_file(): + common_ancestor = common_ancestor.parent return common_ancestor -def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]: +def get_dirs_from_args(args: Iterable[str]) -> List[Path]: def is_option(x: str) -> bool: return x.startswith("-") def get_file_part_from_node_id(x: str) -> str: return x.split("::")[0] - def get_dir_from_path(path: py.path.local) -> py.path.local: - if path.isdir(): + def get_dir_from_path(path: Path) -> Path: + if path.is_dir(): return path - return py.path.local(path.dirname) + return path.parent + + if sys.version_info < (3, 8): + + def safe_exists(path: Path) -> bool: + # On Python<3.8, this can throw on paths that contain characters + # unrepresentable at the OS level. + try: + return path.exists() + except OSError: + return False + + else: + + def safe_exists(path: Path) -> bool: + return path.exists() # These look like paths but may not exist possible_paths = ( - py.path.local(get_file_part_from_node_id(arg)) + absolutepath(get_file_part_from_node_id(arg)) for arg in args if not is_option(arg) ) - return [get_dir_from_path(path) for path in possible_paths if path.exists()] + return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)] CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." @@ -156,15 +176,15 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte def determine_setup( inifile: Optional[str], - args: List[str], + args: Sequence[str], rootdir_cmd_arg: Optional[str] = None, config: Optional["Config"] = None, -) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]: +) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: rootdir = None dirs = get_dirs_from_args(args) if inifile: - inipath_ = py.path.local(inifile) - inipath = inipath_ # type: Optional[py.path.local] + inipath_ = absolutepath(inifile) + inipath = inipath_ # type: Optional[Path] inicfg = load_config_dict_from_file(inipath_) or {} if rootdir_cmd_arg is None: rootdir = get_common_ancestor(dirs) @@ -172,8 +192,10 @@ def determine_setup( ancestor = get_common_ancestor(dirs) rootdir, inipath, inicfg = locate_config([ancestor]) if rootdir is None and rootdir_cmd_arg is None: - for possible_rootdir in ancestor.parts(reverse=True): - if possible_rootdir.join("setup.py").exists(): + for possible_rootdir in itertools.chain( + (ancestor,), reversed(ancestor.parents) + ): + if (possible_rootdir / "setup.py").is_file(): rootdir = possible_rootdir break else: @@ -181,16 +203,16 @@ def determine_setup( rootdir, inipath, inicfg = locate_config(dirs) if rootdir is None: if config is not None: - cwd = config.invocation_dir + cwd = config.invocation_params.dir else: - cwd = py.path.local() + cwd = Path.cwd() rootdir = get_common_ancestor([cwd, ancestor]) is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root: rootdir = ancestor if rootdir_cmd_arg: - rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg)) - if not rootdir.isdir(): + rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) + if not rootdir.is_dir(): raise UsageError( "Directory '{}' not found. Check your '--rootdir' option.".format( rootdir diff --git a/testing/test_config.py b/testing/test_config.py index 26d2a3ef0..346edb330 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -21,17 +21,27 @@ from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import locate_config +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path +from _pytest.pytester import Testdir class TestParseIni: @pytest.mark.parametrize( "section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")] ) - def test_getcfg_and_config(self, testdir, tmpdir, section, filename): - sub = tmpdir.mkdir("sub") - sub.chdir() - tmpdir.join(filename).write( + def test_getcfg_and_config( + self, + testdir: Testdir, + tmp_path: Path, + section: str, + filename: str, + monkeypatch: MonkeyPatch, + ) -> None: + sub = tmp_path / "sub" + sub.mkdir() + monkeypatch.chdir(sub) + (tmp_path / filename).write_text( textwrap.dedent( """\ [{section}] @@ -39,17 +49,14 @@ class TestParseIni: """.format( section=section ) - ) + ), + encoding="utf-8", ) _, _, cfg = locate_config([sub]) assert cfg["name"] == "value" - config = testdir.parseconfigure(sub) + config = testdir.parseconfigure(str(sub)) assert config.inicfg["name"] == "value" - def test_getcfg_empty_path(self): - """Correctly handle zero length arguments (a la pytest '').""" - locate_config([""]) - def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): p1 = testdir.makepyfile("def test(): pass") testdir.makefile( @@ -1168,16 +1175,17 @@ def test_collect_pytest_prefix_bug(pytestconfig): class TestRootdir: - def test_simple_noini(self, tmpdir): - assert get_common_ancestor([tmpdir]) == tmpdir - a = tmpdir.mkdir("a") - assert get_common_ancestor([a, tmpdir]) == tmpdir - assert get_common_ancestor([tmpdir, a]) == tmpdir - with tmpdir.as_cwd(): - assert get_common_ancestor([]) == tmpdir - no_path = tmpdir.join("does-not-exist") - assert get_common_ancestor([no_path]) == tmpdir - assert get_common_ancestor([no_path.join("a")]) == tmpdir + def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + assert get_common_ancestor([tmp_path]) == tmp_path + a = tmp_path / "a" + a.mkdir() + assert get_common_ancestor([a, tmp_path]) == tmp_path + assert get_common_ancestor([tmp_path, a]) == tmp_path + monkeypatch.chdir(tmp_path) + assert get_common_ancestor([]) == tmp_path + no_path = tmp_path / "does-not-exist" + assert get_common_ancestor([no_path]) == tmp_path + assert get_common_ancestor([no_path / "a"]) == tmp_path @pytest.mark.parametrize( "name, contents", @@ -1190,44 +1198,49 @@ class TestRootdir: pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), ], ) - def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None: - inifile = tmpdir.join(name) - inifile.write(contents) + def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: + inipath = tmp_path / name + inipath.write_text(contents, "utf-8") - a = tmpdir.mkdir("a") - b = a.mkdir("b") - for args in ([str(tmpdir)], [str(a)], [str(b)]): - rootdir, parsed_inifile, _ = determine_setup(None, args) - assert rootdir == tmpdir - assert parsed_inifile == inifile - rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)]) - assert rootdir == tmpdir - assert parsed_inifile == inifile + a = tmp_path / "a" + a.mkdir() + b = a / "b" + b.mkdir() + for args in ([str(tmp_path)], [str(a)], [str(b)]): + rootpath, parsed_inipath, _ = determine_setup(None, args) + assert rootpath == tmp_path + assert parsed_inipath == inipath + rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)]) + assert rootpath == tmp_path + assert parsed_inipath == inipath assert ini_config == {"x": "10"} - @pytest.mark.parametrize("name", "setup.cfg tox.ini".split()) - def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None: - inifile = tmpdir.ensure("pytest.ini") - a = tmpdir.mkdir("a") - a.ensure(name) - rootdir, parsed_inifile, _ = determine_setup(None, [str(a)]) - assert rootdir == tmpdir - assert parsed_inifile == inifile + @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"]) + def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None: + inipath = tmp_path / "pytest.ini" + inipath.touch() + a = tmp_path / "a" + a.mkdir() + (a / name).touch() + rootpath, parsed_inipath, _ = determine_setup(None, [str(a)]) + assert rootpath == tmp_path + assert parsed_inipath == inipath - def test_setuppy_fallback(self, tmpdir: py.path.local) -> None: - a = tmpdir.mkdir("a") - a.ensure("setup.cfg") - tmpdir.ensure("setup.py") - rootdir, inifile, inicfg = determine_setup(None, [str(a)]) - assert rootdir == tmpdir - assert inifile is None + def test_setuppy_fallback(self, tmp_path: Path) -> None: + a = tmp_path / "a" + a.mkdir() + (a / "setup.cfg").touch() + (tmp_path / "setup.py").touch() + rootpath, inipath, inicfg = determine_setup(None, [str(a)]) + assert rootpath == tmp_path + assert inipath is None assert inicfg == {} - def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: - monkeypatch.chdir(str(tmpdir)) - rootdir, inifile, inicfg = determine_setup(None, [str(tmpdir)]) - assert rootdir == tmpdir - assert inifile is None + def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + monkeypatch.chdir(tmp_path) + rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)]) + assert rootpath == tmp_path + assert inipath is None assert inicfg == {} @pytest.mark.parametrize( @@ -1242,45 +1255,58 @@ class TestRootdir: ], ) def test_with_specific_inifile( - self, tmpdir: py.path.local, name: str, contents: str + self, tmp_path: Path, name: str, contents: str ) -> None: - p = tmpdir.ensure(name) - p.write(contents) - rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)]) - assert rootdir == tmpdir - assert inifile == p + p = tmp_path / name + p.touch() + p.write_text(contents, "utf-8") + rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)]) + assert rootpath == tmp_path + assert inipath == p assert ini_config == {"x": "10"} - def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None: - monkeypatch.chdir(str(tmpdir)) - a = tmpdir.mkdir("a") - b = tmpdir.mkdir("b") - rootdir, inifile, _ = determine_setup(None, [str(a), str(b)]) - assert rootdir == tmpdir + def test_with_arg_outside_cwd_without_inifile( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + a = tmp_path / "a" + a.mkdir() + b = tmp_path / "b" + b.mkdir() + rootpath, inifile, _ = determine_setup(None, [str(a), str(b)]) + assert rootpath == tmp_path assert inifile is None - def test_with_arg_outside_cwd_with_inifile(self, tmpdir) -> None: - a = tmpdir.mkdir("a") - b = tmpdir.mkdir("b") - inifile = a.ensure("pytest.ini") - rootdir, parsed_inifile, _ = determine_setup(None, [str(a), str(b)]) - assert rootdir == a - assert inifile == parsed_inifile + def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None: + a = tmp_path / "a" + a.mkdir() + b = tmp_path / "b" + b.mkdir() + inipath = a / "pytest.ini" + inipath.touch() + rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)]) + assert rootpath == a + assert inipath == parsed_inipath @pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"])) - def test_with_non_dir_arg(self, dirs, tmpdir) -> None: - with tmpdir.ensure(dir=True).as_cwd(): - rootdir, inifile, _ = determine_setup(None, dirs) - assert rootdir == tmpdir - assert inifile is None + def test_with_non_dir_arg( + self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + monkeypatch.chdir(tmp_path) + rootpath, inipath, _ = determine_setup(None, dirs) + assert rootpath == tmp_path + assert inipath is None - def test_with_existing_file_in_subdir(self, tmpdir) -> None: - a = tmpdir.mkdir("a") - a.ensure("exist") - with tmpdir.as_cwd(): - rootdir, inifile, _ = determine_setup(None, ["a/exist"]) - assert rootdir == tmpdir - assert inifile is None + def test_with_existing_file_in_subdir( + self, tmp_path: Path, monkeypatch: MonkeyPatch + ) -> None: + a = tmp_path / "a" + a.mkdir() + (a / "exists").touch() + monkeypatch.chdir(tmp_path) + rootpath, inipath, _ = determine_setup(None, ["a/exist"]) + assert rootpath == tmp_path + assert inipath is None class TestOverrideIniArgs: diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index 3de2ea218..acb982b4c 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -1,74 +1,74 @@ from textwrap import dedent -import py - import pytest from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import load_config_dict_from_file +from _pytest.pathlib import Path class TestLoadConfigDictFromFile: - def test_empty_pytest_ini(self, tmpdir): + def test_empty_pytest_ini(self, tmp_path: Path) -> None: """pytest.ini files are always considered for configuration, even if empty""" - fn = tmpdir.join("pytest.ini") - fn.write("") + fn = tmp_path / "pytest.ini" + fn.write_text("", encoding="utf-8") assert load_config_dict_from_file(fn) == {} - def test_pytest_ini(self, tmpdir): + def test_pytest_ini(self, tmp_path: Path) -> None: """[pytest] section in pytest.ini files is read correctly""" - fn = tmpdir.join("pytest.ini") - fn.write("[pytest]\nx=1") + fn = tmp_path / "pytest.ini" + fn.write_text("[pytest]\nx=1", encoding="utf-8") assert load_config_dict_from_file(fn) == {"x": "1"} - def test_custom_ini(self, tmpdir): + def test_custom_ini(self, tmp_path: Path) -> None: """[pytest] section in any .ini file is read correctly""" - fn = tmpdir.join("custom.ini") - fn.write("[pytest]\nx=1") + fn = tmp_path / "custom.ini" + fn.write_text("[pytest]\nx=1", encoding="utf-8") assert load_config_dict_from_file(fn) == {"x": "1"} - def test_custom_ini_without_section(self, tmpdir): + def test_custom_ini_without_section(self, tmp_path: Path) -> None: """Custom .ini files without [pytest] section are not considered for configuration""" - fn = tmpdir.join("custom.ini") - fn.write("[custom]") + fn = tmp_path / "custom.ini" + fn.write_text("[custom]", encoding="utf-8") assert load_config_dict_from_file(fn) is None - def test_custom_cfg_file(self, tmpdir): + def test_custom_cfg_file(self, tmp_path: Path) -> None: """Custom .cfg files without [tool:pytest] section are not considered for configuration""" - fn = tmpdir.join("custom.cfg") - fn.write("[custom]") + fn = tmp_path / "custom.cfg" + fn.write_text("[custom]", encoding="utf-8") assert load_config_dict_from_file(fn) is None - def test_valid_cfg_file(self, tmpdir): + def test_valid_cfg_file(self, tmp_path: Path) -> None: """Custom .cfg files with [tool:pytest] section are read correctly""" - fn = tmpdir.join("custom.cfg") - fn.write("[tool:pytest]\nx=1") + fn = tmp_path / "custom.cfg" + fn.write_text("[tool:pytest]\nx=1", encoding="utf-8") assert load_config_dict_from_file(fn) == {"x": "1"} - def test_unsupported_pytest_section_in_cfg_file(self, tmpdir): + def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None: """.cfg files with [pytest] section are no longer supported and should fail to alert users""" - fn = tmpdir.join("custom.cfg") - fn.write("[pytest]") + fn = tmp_path / "custom.cfg" + fn.write_text("[pytest]", encoding="utf-8") with pytest.raises(pytest.fail.Exception): load_config_dict_from_file(fn) - def test_invalid_toml_file(self, tmpdir): + def test_invalid_toml_file(self, tmp_path: Path) -> None: """.toml files without [tool.pytest.ini_options] are not considered for configuration.""" - fn = tmpdir.join("myconfig.toml") - fn.write( + fn = tmp_path / "myconfig.toml" + fn.write_text( dedent( """ [build_system] x = 1 """ - ) + ), + encoding="utf-8", ) assert load_config_dict_from_file(fn) is None - def test_valid_toml_file(self, tmpdir): + def test_valid_toml_file(self, tmp_path: Path) -> None: """.toml files with [tool.pytest.ini_options] are read correctly, including changing data types to str/list for compatibility with other configuration options.""" - fn = tmpdir.join("myconfig.toml") - fn.write( + fn = tmp_path / "myconfig.toml" + fn.write_text( dedent( """ [tool.pytest.ini_options] @@ -77,7 +77,8 @@ class TestLoadConfigDictFromFile: values = ["tests", "integration"] name = "foo" """ - ) + ), + encoding="utf-8", ) assert load_config_dict_from_file(fn) == { "x": "1", @@ -88,23 +89,22 @@ class TestLoadConfigDictFromFile: class TestCommonAncestor: - def test_has_ancestor(self, tmpdir): - fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1) - fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1) - assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo") - assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join( - "foo" - ) - assert get_common_ancestor( - [py.path.local(fn1.dirname), py.path.local(fn2.dirname)] - ) == tmpdir.join("foo") - assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join( - "foo" - ) + def test_has_ancestor(self, tmp_path: Path) -> None: + fn1 = tmp_path / "foo" / "bar" / "test_1.py" + fn1.parent.mkdir(parents=True) + fn1.touch() + fn2 = tmp_path / "foo" / "zaz" / "test_2.py" + fn2.parent.mkdir(parents=True) + fn2.touch() + assert get_common_ancestor([fn1, fn2]) == tmp_path / "foo" + assert get_common_ancestor([fn1.parent, fn2]) == tmp_path / "foo" + assert get_common_ancestor([fn1.parent, fn2.parent]) == tmp_path / "foo" + assert get_common_ancestor([fn1, fn2.parent]) == tmp_path / "foo" - def test_single_dir(self, tmpdir): - assert get_common_ancestor([tmpdir]) == tmpdir + def test_single_dir(self, tmp_path: Path) -> None: + assert get_common_ancestor([tmp_path]) == tmp_path - def test_single_file(self, tmpdir): - fn = tmpdir.join("foo.py").ensure(file=1) - assert get_common_ancestor([fn]) == tmpdir + def test_single_file(self, tmp_path: Path) -> None: + fn = tmp_path / "foo.py" + fn.touch() + assert get_common_ancestor([fn]) == tmp_path From f8c4e038fde9e58732ef6ffad77cc25d0746cbc3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 2 Aug 2020 17:56:36 +0300 Subject: [PATCH 4/4] Replace some usages of py.path.local --- extra/get_issues.py | 8 ++++---- src/_pytest/_code/code.py | 27 ++++++++++++++++++--------- src/_pytest/compat.py | 14 +++++++++----- src/_pytest/config/__init__.py | 4 ++-- src/_pytest/fixtures.py | 5 +++-- src/_pytest/python.py | 6 +++--- src/_pytest/resultlog.py | 4 +--- testing/acceptance_test.py | 2 +- 8 files changed, 41 insertions(+), 29 deletions(-) diff --git a/extra/get_issues.py b/extra/get_issues.py index c264b2644..4aaa3c3ec 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -1,6 +1,6 @@ import json +from pathlib import Path -import py import requests issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues" @@ -31,12 +31,12 @@ def get_issues(): def main(args): - cachefile = py.path.local(args.cache) + cachefile = Path(args.cache) if not cachefile.exists() or args.refresh: issues = get_issues() - cachefile.write(json.dumps(issues)) + cachefile.write_text(json.dumps(issues), "utf-8") else: - issues = json.loads(cachefile.read()) + issues = json.loads(cachefile.read_text("utf-8")) open_issues = [x for x in issues if x["state"] == "open"] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b2e4fcd33..420135b4e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -41,6 +41,7 @@ from _pytest.compat import ATTRS_EQ_FIELD from _pytest.compat import get_real_func from _pytest.compat import overload from _pytest.compat import TYPE_CHECKING +from _pytest.pathlib import Path if TYPE_CHECKING: from typing import Type @@ -1190,12 +1191,12 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]: # note: if we need to add more paths than what we have now we should probably use a list # for better maintenance. -_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) +_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc")) # pluggy is either a package or a single module depending on the version -if _PLUGGY_DIR.basename == "__init__.py": - _PLUGGY_DIR = _PLUGGY_DIR.dirpath() -_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() -_PY_DIR = py.path.local(py.__file__).dirpath() +if _PLUGGY_DIR.name == "__init__.py": + _PLUGGY_DIR = _PLUGGY_DIR.parent +_PYTEST_DIR = Path(_pytest.__file__).parent +_PY_DIR = Path(py.__file__).parent def filter_traceback(entry: TracebackEntry) -> bool: @@ -1213,9 +1214,17 @@ def filter_traceback(entry: TracebackEntry) -> bool: is_generated = "<" in raw_filename and ">" in raw_filename if is_generated: return False + # entry.path might point to a non-existing file, in which case it will # also return a str object. See #1133. - p = py.path.local(entry.path) - return ( - not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) - ) + p = Path(entry.path) + + parents = p.parents + if _PLUGGY_DIR in parents: + return False + if _PYTEST_DIR in parents: + return False + if _PY_DIR in parents: + return False + + return True diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index ff98492dc..4b46d9c95 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -18,7 +18,6 @@ from typing import TypeVar from typing import Union import attr -import py from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail @@ -104,13 +103,18 @@ def is_async_function(func: object) -> bool: ) -def getlocation(function, curdir=None) -> str: +def getlocation(function, curdir: Optional[str] = None) -> str: + from _pytest.pathlib import Path + function = get_real_func(function) - fn = py.path.local(inspect.getfile(function)) + fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno if curdir is not None: - relfn = fn.relto(curdir) - if relfn: + try: + relfn = fn.relative_to(curdir) + except ValueError: + pass + else: return "%s:%d" % (relfn, lineno + 1) return "%s:%d" % (fn, lineno + 1) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6305cdbd5..453dd8345 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -123,7 +123,7 @@ def filter_traceback_for_conftest_import_failure( def main( - args: Optional[List[str]] = None, + args: Optional[Union[List[str], py.path.local]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, ) -> Union[int, ExitCode]: """Perform an in-process test run. @@ -1308,7 +1308,7 @@ class Config: values = [] # type: List[py.path.local] for relroot in relroots: if not isinstance(relroot, py.path.local): - relroot = relroot.replace("/", py.path.local.sep) + relroot = relroot.replace("/", os.sep) relroot = modpath.join(relroot, abs=True) values.append(relroot) return values diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d2ff6203b..846cc2bb1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1,5 +1,6 @@ import functools import inspect +import os import sys import warnings from collections import defaultdict @@ -1515,8 +1516,8 @@ class FixtureManager: # by their test id). if p.basename.startswith("conftest.py"): nodeid = p.dirpath().relto(self.config.rootdir) - if p.sep != nodes.SEP: - nodeid = nodeid.replace(p.sep, nodes.SEP) + if os.sep != nodes.SEP: + nodeid = nodeid.replace(os.sep, nodes.SEP) self.parsefactories(plugin, nodeid) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 741624565..0661f3402 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1339,7 +1339,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: verbose = config.getvalue("verbose") def get_best_relpath(func): - loc = getlocation(func, curdir) + loc = getlocation(func, str(curdir)) return curdir.bestrelpath(py.path.local(loc)) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: @@ -1404,7 +1404,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if not fixturedefs: continue for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, curdir) + loc = getlocation(fixturedef.func, str(curdir)) if (fixturedef.argname, loc) in seen: continue seen.add((fixturedef.argname, loc)) @@ -1434,7 +1434,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if verbose > 0: tw.write(" -- %s" % bestrel, yellow=True) tw.write("\n") - loc = getlocation(fixturedef.func, curdir) + loc = getlocation(fixturedef.func, str(curdir)) doc = inspect.getdoc(fixturedef.func) if doc: write_docstring(tw, doc) diff --git a/src/_pytest/resultlog.py b/src/_pytest/resultlog.py index c043c749f..686f7f3b0 100644 --- a/src/_pytest/resultlog.py +++ b/src/_pytest/resultlog.py @@ -3,8 +3,6 @@ import os from typing import IO from typing import Union -import py - from _pytest._code.code import ExceptionRepr from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -106,5 +104,5 @@ class ResultLog: if excrepr.reprcrash is not None: path = excrepr.reprcrash.path else: - path = "cwd:%s" % py.path.local() + path = "cwd:%s" % os.getcwd() self.write_log_entry(path, "!", str(excrepr)) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3172dad7c..b37cfa0cb 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -586,7 +586,7 @@ class TestInvocationVariants: ): pytest.main("-h") # type: ignore[arg-type] - def test_invoke_with_path(self, tmpdir, capsys): + def test_invoke_with_path(self, tmpdir: py.path.local, capsys) -> None: retcode = pytest.main(tmpdir) assert retcode == ExitCode.NO_TESTS_COLLECTED out, err = capsys.readouterr()