From 70f3ad1c1f31b35d4004f92734b4afd6c8fbdecf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 3 Aug 2020 17:46:35 +0300 Subject: [PATCH] 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 | 198 ++++++++++++++++++-------------- testing/test_findpaths.py | 102 ++++++++-------- 4 files changed, 227 insertions(+), 176 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 455a14b4040..6305cdbd57d 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1006,12 +1006,15 @@ def _initini(self, args: Sequence[str]) -> None: 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 be25fc82920..65120e48418 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 make_scalar(v: object) -> Union[str, List[str]]: 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 @@ def get_dir_from_path(path: py.path.local) -> py.path.local: 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 26d2a3ef09b..346edb3304c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -21,17 +21,27 @@ 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 @@ def test_getcfg_and_config(self, testdir, tmpdir, section, filename): """.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 @@ class pytest_something: 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 @@ def test_simple_noini(self, tmpdir): 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) - - 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 + def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: + inipath = tmp_path / name + inipath.write_text(contents, "utf-8") + + 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 - - 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 + @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, 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 @@ def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: ], ) 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_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_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, 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 3de2ea21828..acb982b4cf4 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 @@ def test_valid_toml_file(self, tmpdir): values = ["tests", "integration"] name = "foo" """ - ) + ), + encoding="utf-8", ) assert load_config_dict_from_file(fn) == { "x": "1", @@ -88,23 +89,22 @@ def test_valid_toml_file(self, tmpdir): 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_single_dir(self, tmpdir): - assert get_common_ancestor([tmpdir]) == tmpdir - - def test_single_file(self, tmpdir): - fn = tmpdir.join("foo.py").ensure(file=1) - assert get_common_ancestor([fn]) == tmpdir + 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, tmp_path: Path) -> None: + assert get_common_ancestor([tmp_path]) == tmp_path + + def test_single_file(self, tmp_path: Path) -> None: + fn = tmp_path / "foo.py" + fn.touch() + assert get_common_ancestor([fn]) == tmp_path