diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 157c36490..e4e8f700a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -541,6 +541,7 @@ class PytestPluginManager(PluginManager): noconftest: bool, rootpath: Path, confcutdir: Optional[Path], + invocation_dir: Path, importmode: Union[ImportMode, str], ) -> None: """Load initial conftest files given a preparsed "namespace". @@ -550,8 +551,9 @@ class PytestPluginManager(PluginManager): All builtin and 3rd party plugins will have been loaded, however, so common options will not confuse our logic here. """ - current = Path.cwd() - self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None + self._confcutdir = ( + absolutepath(invocation_dir / confcutdir) if confcutdir else None + ) self._noconftest = noconftest self._using_pyargs = pyargs foundanchor = False @@ -561,7 +563,7 @@ class PytestPluginManager(PluginManager): i = path.find("::") if i != -1: path = path[:i] - anchor = absolutepath(current / path) + anchor = absolutepath(invocation_dir / path) # Ensure we do not break if what appears to be an anchor # is in fact a very long option (#10169, #11394). @@ -569,7 +571,7 @@ class PytestPluginManager(PluginManager): self._try_load_conftest(anchor, importmode, rootpath) foundanchor = True if not foundanchor: - self._try_load_conftest(current, importmode, rootpath) + self._try_load_conftest(invocation_dir, importmode, rootpath) def _is_in_confcutdir(self, path: Path) -> bool: """Whether a path is within the confcutdir. @@ -1168,6 +1170,7 @@ class Config: noconftest=early_config.known_args_namespace.noconftest, rootpath=early_config.rootpath, confcutdir=early_config.known_args_namespace.confcutdir, + invocation_dir=early_config.invocation_params.dir, importmode=early_config.known_args_namespace.importmode, ) @@ -1176,8 +1179,8 @@ class Config: args, namespace=copy.copy(self.option) ) rootpath, inipath, inicfg = determine_setup( - ns.inifilename, - ns.file_or_dir + unknown_args, + inifile=ns.inifilename, + args=ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, invocation_dir=self.invocation_params.dir, ) @@ -1261,6 +1264,8 @@ class Config: """Decide the args (initial paths/nodeids) to use given the relevant inputs. :param warn: Whether can issue warnings. + + :returns: The args and the args source. Guaranteed to be non-empty. """ if args: source = Config.ArgsSource.ARGS diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index fc30533b6..8e69a46fd 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -87,6 +87,7 @@ def load_config_dict_from_file( def locate_config( + invocation_dir: Path, args: Iterable[Path], ) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]: """Search in the list of arguments for a valid ini-file for pytest, @@ -100,7 +101,7 @@ def locate_config( ] args = [x for x in args if not str(x).startswith("-")] if not args: - args = [Path.cwd()] + args = [invocation_dir] for arg in args: argpath = absolutepath(arg) for base in (argpath, *argpath.parents): @@ -113,7 +114,10 @@ def locate_config( return None, None, {} -def get_common_ancestor(paths: Iterable[Path]) -> Path: +def get_common_ancestor( + invocation_dir: Path, + paths: Iterable[Path], +) -> Path: common_ancestor: Optional[Path] = None for path in paths: if not path.exists(): @@ -130,7 +134,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path: if shared is not None: common_ancestor = shared if common_ancestor is None: - common_ancestor = Path.cwd() + common_ancestor = invocation_dir elif common_ancestor.is_file(): common_ancestor = common_ancestor.parent return common_ancestor @@ -162,10 +166,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte def determine_setup( + *, inifile: Optional[str], args: Sequence[str], - rootdir_cmd_arg: Optional[str] = None, - invocation_dir: Optional[Path] = None, + rootdir_cmd_arg: Optional[str], + invocation_dir: Path, ) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: """Determine the rootdir, inifile and ini configuration values from the command line arguments. @@ -177,8 +182,7 @@ def determine_setup( :param rootdir_cmd_arg: The `--rootdir` command line argument, if given. :param invocation_dir: - The working directory when pytest was invoked, if known. - If not known, the current working directory is used. + The working directory when pytest was invoked. """ rootdir = None dirs = get_dirs_from_args(args) @@ -189,8 +193,8 @@ def determine_setup( if rootdir_cmd_arg is None: rootdir = inipath_.parent else: - ancestor = get_common_ancestor(dirs) - rootdir, inipath, inicfg = locate_config([ancestor]) + ancestor = get_common_ancestor(invocation_dir, dirs) + rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor]) if rootdir is None and rootdir_cmd_arg is None: for possible_rootdir in (ancestor, *ancestor.parents): if (possible_rootdir / "setup.py").is_file(): @@ -198,13 +202,11 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inipath, inicfg = locate_config(dirs) + rootdir, inipath, inicfg = locate_config(invocation_dir, dirs) if rootdir is None: - if invocation_dir is not None: - cwd = invocation_dir - else: - cwd = Path.cwd() - rootdir = get_common_ancestor([cwd, ancestor]) + rootdir = get_common_ancestor( + invocation_dir, [invocation_dir, ancestor] + ) if is_fs_root(rootdir): rootdir = ancestor if rootdir_cmd_arg: diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 364bf4c42..91ea76997 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -109,10 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]: debugfile = open(path, "w", encoding="utf-8") debugfile.write( "versions pytest-%s, " - "python-%s\ncwd=%s\nargs=%s\n\n" + "python-%s\ninvocation_dir=%s\ncwd=%s\nargs=%s\n\n" % ( pytest.__version__, ".".join(map(str, sys.version_info)), + config.invocation_params.dir, os.getcwd(), config.invocation_params.args, ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e55772d5e..64480c971 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1525,14 +1525,13 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) - return val if escape_option else ascii_escaped(val) # type: ignore -def _pretty_fixture_path(func) -> str: - cwd = Path.cwd() - loc = Path(getlocation(func, str(cwd))) +def _pretty_fixture_path(invocation_dir: Path, func) -> str: + loc = Path(getlocation(func, invocation_dir)) prefix = Path("...", "_pytest") try: return str(prefix / loc.relative_to(_PYTEST_DIR)) except ValueError: - return bestrelpath(cwd, loc) + return bestrelpath(invocation_dir, loc) def show_fixtures_per_test(config): @@ -1545,19 +1544,19 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = Path.cwd() + invocation_dir = config.invocation_params.dir tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") def get_best_relpath(func) -> str: - loc = getlocation(func, str(curdir)) - return bestrelpath(curdir, Path(loc)) + loc = getlocation(func, invocation_dir) + return bestrelpath(invocation_dir, Path(loc)) def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: argname = fixture_def.argname if verbose <= 0 and argname.startswith("_"): return - prettypath = _pretty_fixture_path(fixture_def.func) + prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func) tw.write(f"{argname}", green=True) tw.write(f" -- {prettypath}", yellow=True) tw.write("\n") @@ -1601,7 +1600,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() - curdir = Path.cwd() + invocation_dir = config.invocation_params.dir tw = _pytest.config.create_terminal_writer(config) verbose = config.getvalue("verbose") @@ -1615,7 +1614,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: if not fixturedefs: continue for fixturedef in fixturedefs: - loc = getlocation(fixturedef.func, str(curdir)) + loc = getlocation(fixturedef.func, invocation_dir) if (fixturedef.argname, loc) in seen: continue seen.add((fixturedef.argname, loc)) @@ -1623,7 +1622,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: ( len(fixturedef.baseid), fixturedef.func.__module__, - _pretty_fixture_path(fixturedef.func), + _pretty_fixture_path(invocation_dir, fixturedef.func), fixturedef.argname, fixturedef, ) diff --git a/testing/test_config.py b/testing/test_config.py index 0f586c043..1eb530ace 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -59,7 +59,7 @@ class TestParseIni: ), encoding="utf-8", ) - _, _, cfg = locate_config([sub]) + _, _, cfg = locate_config(Path.cwd(), [sub]) assert cfg["name"] == "value" config = pytester.parseconfigure(str(sub)) assert config.inicfg["name"] == "value" @@ -1436,16 +1436,16 @@ def test_collect_pytest_prefix_bug(pytestconfig): class TestRootdir: def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - assert get_common_ancestor([tmp_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [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 + assert get_common_ancestor(Path.cwd(), [a, tmp_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [tmp_path, a]) == tmp_path monkeypatch.chdir(tmp_path) - assert get_common_ancestor([]) == tmp_path + assert get_common_ancestor(Path.cwd(), []) == 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 + assert get_common_ancestor(Path.cwd(), [no_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [no_path / "a"]) == tmp_path @pytest.mark.parametrize( "name, contents", @@ -1467,10 +1467,20 @@ class TestRootdir: b = a / "b" b.mkdir() for args in ([str(tmp_path)], [str(a)], [str(b)]): - rootpath, parsed_inipath, _ = determine_setup(None, args) + rootpath, parsed_inipath, _ = determine_setup( + inifile=None, + args=args, + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert parsed_inipath == inipath - rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)]) + rootpath, parsed_inipath, ini_config = determine_setup( + inifile=None, + args=[str(b), str(a)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert parsed_inipath == inipath assert ini_config == {"x": "10"} @@ -1482,7 +1492,12 @@ class TestRootdir: a = tmp_path / "a" a.mkdir() (a / name).touch() - rootpath, parsed_inipath, _ = determine_setup(None, [str(a)]) + rootpath, parsed_inipath, _ = determine_setup( + inifile=None, + args=[str(a)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert parsed_inipath == inipath @@ -1491,14 +1506,24 @@ class TestRootdir: a.mkdir() (a / "setup.cfg").touch() (tmp_path / "setup.py").touch() - rootpath, inipath, inicfg = determine_setup(None, [str(a)]) + rootpath, inipath, inicfg = determine_setup( + inifile=None, + args=[str(a)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None assert inicfg == {} def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) - rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)]) + rootpath, inipath, inicfg = determine_setup( + inifile=None, + args=[str(tmp_path)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None assert inicfg == {} @@ -1520,7 +1545,12 @@ class TestRootdir: p = tmp_path / name p.touch() p.write_text(contents, encoding="utf-8") - rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)]) + rootpath, inipath, ini_config = determine_setup( + inifile=str(p), + args=[str(tmp_path)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath == p assert ini_config == {"x": "10"} @@ -1534,14 +1564,24 @@ class TestRootdir: monkeypatch.chdir(tmp_path) # No config file is explicitly given: rootdir is determined to be cwd. - rootpath, found_inipath, *_ = determine_setup(None, [str(tests_dir)]) + rootpath, found_inipath, *_ = determine_setup( + inifile=None, + args=[str(tests_dir)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert found_inipath is None # Config file is explicitly given: rootdir is determined to be inifile's directory. inipath = tmp_path / "pytest.ini" inipath.touch() - rootpath, found_inipath, *_ = determine_setup(str(inipath), [str(tests_dir)]) + rootpath, found_inipath, *_ = determine_setup( + inifile=str(inipath), + args=[str(tests_dir)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert found_inipath == inipath @@ -1553,7 +1593,12 @@ class TestRootdir: a.mkdir() b = tmp_path / "b" b.mkdir() - rootpath, inifile, _ = determine_setup(None, [str(a), str(b)]) + rootpath, inifile, _ = determine_setup( + inifile=None, + args=[str(a), str(b)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inifile is None @@ -1564,7 +1609,12 @@ class TestRootdir: b.mkdir() inipath = a / "pytest.ini" inipath.touch() - rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)]) + rootpath, parsed_inipath, _ = determine_setup( + inifile=None, + args=[str(a), str(b)], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == a assert inipath == parsed_inipath @@ -1573,7 +1623,12 @@ class TestRootdir: self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) - rootpath, inipath, _ = determine_setup(None, dirs) + rootpath, inipath, _ = determine_setup( + inifile=None, + args=dirs, + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None @@ -1584,7 +1639,12 @@ class TestRootdir: a.mkdir() (a / "exists").touch() monkeypatch.chdir(tmp_path) - rootpath, inipath, _ = determine_setup(None, ["a/exist"]) + rootpath, inipath, _ = determine_setup( + inifile=None, + args=["a/exist"], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path assert inipath is None @@ -1598,7 +1658,12 @@ class TestRootdir: (tmp_path / "myproject" / "tests").mkdir() monkeypatch.chdir(tmp_path / "myproject") - rootpath, inipath, _ = determine_setup(None, ["tests/"]) + rootpath, inipath, _ = determine_setup( + inifile=None, + args=["tests/"], + rootdir_cmd_arg=None, + invocation_dir=Path.cwd(), + ) assert rootpath == tmp_path / "myproject" assert inipath == tmp_path / "myproject" / "setup.cfg" diff --git a/testing/test_conftest.py b/testing/test_conftest.py index cfc2d577b..e74190727 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -35,6 +35,7 @@ def conftest_setinitial( noconftest=False, rootpath=Path(args[0]), confcutdir=confcutdir, + invocation_dir=Path.cwd(), importmode="prepend", ) diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index 8287de603..65370a0ba 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -109,18 +109,19 @@ class TestCommonAncestor: 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" + cwd = Path.cwd() + assert get_common_ancestor(cwd, [fn1, fn2]) == tmp_path / "foo" + assert get_common_ancestor(cwd, [fn1.parent, fn2]) == tmp_path / "foo" + assert get_common_ancestor(cwd, [fn1.parent, fn2.parent]) == tmp_path / "foo" + assert get_common_ancestor(cwd, [fn1, fn2.parent]) == tmp_path / "foo" def test_single_dir(self, tmp_path: Path) -> None: - assert get_common_ancestor([tmp_path]) == tmp_path + assert get_common_ancestor(Path.cwd(), [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 + assert get_common_ancestor(Path.cwd(), [fn]) == tmp_path def test_get_dirs_from_args(tmp_path):