diff --git a/AUTHORS b/AUTHORS index cb8420e4a02..9539e8dc4f4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -403,6 +403,7 @@ Sadra Barikbin Saiprasad Kale Samuel Colvin Samuel Dion-Girardeau +Samuel Gaist Samuel Jirovec Samuel Searles-Bryant Samuel Therrien (Avasam) diff --git a/changelog/13330.improvement.rst b/changelog/13330.improvement.rst new file mode 100644 index 00000000000..226b908881d --- /dev/null +++ b/changelog/13330.improvement.rst @@ -0,0 +1,3 @@ +Having both ``pytest.ini`` and ``pyproject.toml`` will now print a warning to make it clearer to the user that the former takes precedence over the latter. + +-- by :user:`sgaist` diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 38fb1ee6d27..037eb867595 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1107,6 +1107,14 @@ def inipath(self) -> pathlib.Path | None: """ return self._inipath + @property + def should_warn(self) -> bool: + """Whether a warning should be emitted for the configuration. + + .. versionadded:: 8.6 + """ + return self._should_warn + def add_cleanup(self, func: Callable[[], None]) -> None: """Add a function to be called when the config object gets out of use (usually coinciding with pytest_unconfigure). @@ -1242,7 +1250,7 @@ def _initini(self, args: Sequence[str]) -> None: ns, unknown_args = self._parser.parse_known_and_unknown_args( args, namespace=copy.copy(self.option) ) - rootpath, inipath, inicfg = determine_setup( + rootpath, inipath, inicfg, should_warn = determine_setup( inifile=ns.inifilename, args=ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, @@ -1250,6 +1258,7 @@ def _initini(self, args: Sequence[str]) -> None: ) self._rootpath = rootpath self._inipath = inipath + self._should_warn = should_warn self.inicfg = inicfg self._parser.extra_info["rootdir"] = str(self.rootpath) self._parser.extra_info["inifile"] = str(self.inipath) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 763803adbe7..c12a1516b06 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -91,7 +91,7 @@ def make_scalar(v: object) -> str | list[str]: def locate_config( invocation_dir: Path, args: Iterable[Path], -) -> tuple[Path | None, Path | None, ConfigDict]: +) -> tuple[Path | None, Path | None, ConfigDict, bool]: """Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict).""" config_names = [ @@ -105,6 +105,7 @@ def locate_config( if not args: args = [invocation_dir] found_pyproject_toml: Path | None = None + for arg in args: argpath = absolutepath(arg) for base in (argpath, *argpath.parents): @@ -115,10 +116,17 @@ def locate_config( found_pyproject_toml = p ini_config = load_config_dict_from_file(p) if ini_config is not None: - return base, p, ini_config + should_warn = False + if ".ini" in p.suffixes: + pyproject = base / "pyproject.toml" + if pyproject.is_file(): + should_warn = ( + load_config_dict_from_file(pyproject) is not None + ) + return base, p, ini_config, should_warn if found_pyproject_toml is not None: - return found_pyproject_toml.parent, found_pyproject_toml, {} - return None, None, {} + return found_pyproject_toml.parent, found_pyproject_toml, {}, False + return None, None, {}, False def get_common_ancestor( @@ -178,7 +186,7 @@ def determine_setup( args: Sequence[str], rootdir_cmd_arg: str | None, invocation_dir: Path, -) -> tuple[Path, Path | None, ConfigDict]: +) -> tuple[Path, Path | None, ConfigDict, bool]: """Determine the rootdir, inifile and ini configuration values from the command line arguments. @@ -193,6 +201,7 @@ def determine_setup( """ rootdir = None dirs = get_dirs_from_args(args) + should_warn = False if inifile: inipath_ = absolutepath(inifile) inipath: Path | None = inipath_ @@ -201,7 +210,9 @@ def determine_setup( rootdir = inipath_.parent else: ancestor = get_common_ancestor(invocation_dir, dirs) - rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor]) + rootdir, inipath, inicfg, should_warn = 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(): @@ -209,7 +220,7 @@ def determine_setup( break else: if dirs != [ancestor]: - rootdir, inipath, inicfg = locate_config(invocation_dir, dirs) + rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs) if rootdir is None: rootdir = get_common_ancestor( invocation_dir, [invocation_dir, ancestor] @@ -223,7 +234,7 @@ def determine_setup( f"Directory '{rootdir}' not found. Check your '--rootdir' option." ) assert rootdir is not None - return rootdir, inipath, inicfg or {} + return rootdir, inipath, inicfg or {}, should_warn def is_fs_root(p: Path) -> bool: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 71ea4b1bab9..e3a8505039f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -879,7 +879,12 @@ def pytest_report_header(self, config: Config) -> list[str]: result = [f"rootdir: {config.rootpath}"] if config.inipath: - result.append("configfile: " + bestrelpath(config.rootpath, config.inipath)) + warning = "" + if config.should_warn: + warning = " (WARNING: ignoring pytest config in pyproject.toml!)" + result.append( + "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning + ) if config.args_source == Config.ArgsSource.TESTPATHS: testpaths: list[str] = config.getini("testpaths") diff --git a/testing/test_config.py b/testing/test_config.py index f2cc139dffa..b3f55e7d25e 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -56,7 +56,7 @@ def test_getcfg_and_config( ), encoding="utf-8", ) - _, _, cfg = locate_config(Path.cwd(), [sub]) + _, _, cfg, _ = locate_config(Path.cwd(), [sub]) assert cfg["name"] == "value" config = pytester.parseconfigure(str(sub)) assert config.inicfg["name"] == "value" @@ -1635,7 +1635,7 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: b = a / "b" b.mkdir() for args in ([str(tmp_path)], [str(a)], [str(b)]): - rootpath, parsed_inipath, _ = determine_setup( + rootpath, parsed_inipath, *_ = determine_setup( inifile=None, args=args, rootdir_cmd_arg=None, @@ -1643,7 +1643,7 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: ) assert rootpath == tmp_path assert parsed_inipath == inipath - rootpath, parsed_inipath, ini_config = determine_setup( + rootpath, parsed_inipath, ini_config, _ = determine_setup( inifile=None, args=[str(b), str(a)], rootdir_cmd_arg=None, @@ -1660,7 +1660,7 @@ def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> Non a = tmp_path / "a" a.mkdir() (a / name).touch() - rootpath, parsed_inipath, _ = determine_setup( + rootpath, parsed_inipath, *_ = determine_setup( inifile=None, args=[str(a)], rootdir_cmd_arg=None, @@ -1674,7 +1674,7 @@ def test_setuppy_fallback(self, tmp_path: Path) -> None: a.mkdir() (a / "setup.cfg").touch() (tmp_path / "setup.py").touch() - rootpath, inipath, inicfg = determine_setup( + rootpath, inipath, inicfg, _ = determine_setup( inifile=None, args=[str(a)], rootdir_cmd_arg=None, @@ -1686,7 +1686,7 @@ def test_setuppy_fallback(self, tmp_path: Path) -> None: def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) - rootpath, inipath, inicfg = determine_setup( + rootpath, inipath, inicfg, _ = determine_setup( inifile=None, args=[str(tmp_path)], rootdir_cmd_arg=None, @@ -1713,7 +1713,7 @@ def test_with_specific_inifile( p = tmp_path / name p.touch() p.write_text(contents, encoding="utf-8") - rootpath, inipath, ini_config = determine_setup( + rootpath, inipath, ini_config, _ = determine_setup( inifile=str(p), args=[str(tmp_path)], rootdir_cmd_arg=None, @@ -1761,7 +1761,7 @@ def test_with_arg_outside_cwd_without_inifile( a.mkdir() b = tmp_path / "b" b.mkdir() - rootpath, inifile, _ = determine_setup( + rootpath, inifile, *_ = determine_setup( inifile=None, args=[str(a), str(b)], rootdir_cmd_arg=None, @@ -1777,7 +1777,7 @@ def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None: b.mkdir() inipath = a / "pytest.ini" inipath.touch() - rootpath, parsed_inipath, _ = determine_setup( + rootpath, parsed_inipath, *_ = determine_setup( inifile=None, args=[str(a), str(b)], rootdir_cmd_arg=None, @@ -1791,7 +1791,7 @@ def test_with_non_dir_arg( self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) - rootpath, inipath, _ = determine_setup( + rootpath, inipath, *_ = determine_setup( inifile=None, args=dirs, rootdir_cmd_arg=None, @@ -1807,7 +1807,7 @@ def test_with_existing_file_in_subdir( a.mkdir() (a / "exists").touch() monkeypatch.chdir(tmp_path) - rootpath, inipath, _ = determine_setup( + rootpath, inipath, *_ = determine_setup( inifile=None, args=["a/exist"], rootdir_cmd_arg=None, @@ -1826,7 +1826,7 @@ def test_with_config_also_in_parent_directory( (tmp_path / "myproject" / "tests").mkdir() monkeypatch.chdir(tmp_path / "myproject") - rootpath, inipath, _ = determine_setup( + rootpath, inipath, *_ = determine_setup( inifile=None, args=["tests/"], rootdir_cmd_arg=None, diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 44ce7aff563..4f9b1b8d776 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2893,6 +2893,68 @@ def test_format_trimmed() -> None: assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) " +def test_warning_when_init_trumps_pyproject_toml( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: + """Regression test for #7814.""" + tests = pytester.path.joinpath("tests") + tests.mkdir() + pytester.makepyprojecttoml( + f""" + [tool.pytest.ini_options] + testpaths = ['{tests}'] + """ + ) + pytester.makefile(".ini", pytest="") + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)", + ] + ) + + +def test_no_warning_when_init_but_pyproject_toml_has_no_entry( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: + """Regression test for #7814.""" + tests = pytester.path.joinpath("tests") + tests.mkdir() + pytester.makepyprojecttoml( + f""" + [tool] + testpaths = ['{tests}'] + """ + ) + pytester.makefile(".ini", pytest="") + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "configfile: pytest.ini", + ] + ) + + +def test_no_warning_on_terminal_with_a_single_config_file( + pytester: Pytester, monkeypatch: MonkeyPatch +) -> None: + """Regression test for #7814.""" + tests = pytester.path.joinpath("tests") + tests.mkdir() + pytester.makepyprojecttoml( + f""" + [tool.pytest.ini_options] + testpaths = ['{tests}'] + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "configfile: pyproject.toml", + ] + ) + + class TestFineGrainedTestCase: DEFAULT_FILE_CONTENTS = """ import pytest