From 5ff1af4e99e2a7135b25acaf2fcdfdc3d97367be Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Tue, 7 Oct 2025 21:09:34 +0200 Subject: [PATCH 1/8] feat: add warning when pytest.ini and pyproject.toml are present The behaviour does not change however users will be notified that pytest.ini is selected over pyproject.toml and any configuration related to pytest it may contain. --- AUTHORS | 1 + changelog/13330.improvement.rst | 3 +++ src/_pytest/terminal.py | 9 +++++++- testing/test_terminal.py | 41 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 changelog/13330.improvement.rst 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/terminal.py b/src/_pytest/terminal.py index 71ea4b1bab9..f752e2cccc8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -879,7 +879,14 @@ 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.inipath.name in ["pytest.ini", ".pytest.ini"]: + pyproject = config.rootpath / "pyproject.toml" + if pyproject.exists(): + 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_terminal.py b/testing/test_terminal.py index 44ce7aff563..f530135cc88 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2893,6 +2893,47 @@ 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_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 From 6eab536e0e19d74c3f88894cf6055fb8c025009b Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Tue, 7 Oct 2025 23:13:06 +0200 Subject: [PATCH 2/8] refactor: move warning info into Config Doing so, Terminal does not need to know anything about the configuration files. --- src/_pytest/config/__init__.py | 11 ++++++++++- src/_pytest/config/findpaths.py | 27 +++++++++++++++++++-------- src/_pytest/terminal.py | 6 ++---- testing/test_config.py | 24 ++++++++++++------------ testing/test_terminal.py | 21 +++++++++++++++++++++ 5 files changed, 64 insertions(+), 25 deletions(-) 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..ebd2587f991 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 p.name in ["pytest.ini", ".pytest.ini"]: + 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 f752e2cccc8..e3a8505039f 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -880,10 +880,8 @@ def pytest_report_header(self, config: Config) -> list[str]: if config.inipath: warning = "" - if config.inipath.name in ["pytest.ini", ".pytest.ini"]: - pyproject = config.rootpath / "pyproject.toml" - if pyproject.exists(): - warning = " (WARNING: ignoring pytest config in pyproject.toml!)" + if config.should_warn: + warning = " (WARNING: ignoring pytest config in pyproject.toml!)" result.append( "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning ) 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 f530135cc88..4f9b1b8d776 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2914,6 +2914,27 @@ def test_warning_when_init_trumps_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: From 9bdbec5c60c1357211b986cfdf09d5e6d9b51839 Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Tue, 7 Oct 2025 23:44:13 +0200 Subject: [PATCH 3/8] refactor: simplify ini check Do it the other way around, check if the file suffix is ini rather than listing the possible names. --- src/_pytest/config/findpaths.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index ebd2587f991..c12a1516b06 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -117,7 +117,7 @@ def locate_config( ini_config = load_config_dict_from_file(p) if ini_config is not None: should_warn = False - if p.name in ["pytest.ini", ".pytest.ini"]: + if ".ini" in p.suffixes: pyproject = base / "pyproject.toml" if pyproject.is_file(): should_warn = ( From 67721954d6d304e84268b6607806e8676746c0ce Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Wed, 8 Oct 2025 12:14:09 +0200 Subject: [PATCH 4/8] refactor: make API private and return list of ignored files This will let users know exactly which files have been ignored in case multiple of them contain pytest configuration. --- src/_pytest/config/__init__.py | 12 ++---------- src/_pytest/config/findpaths.py | 32 +++++++++++++++++--------------- src/_pytest/terminal.py | 4 ++-- testing/test_terminal.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 037eb867595..3b2edf61c77 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1107,14 +1107,6 @@ 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). @@ -1250,7 +1242,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, should_warn = determine_setup( + rootpath, inipath, inicfg, ignored_files = determine_setup( inifile=ns.inifilename, args=ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, @@ -1258,7 +1250,7 @@ def _initini(self, args: Sequence[str]) -> None: ) self._rootpath = rootpath self._inipath = inipath - self._should_warn = should_warn + self._ignored_files = ignored_files 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 c12a1516b06..027a986c5de 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, bool]: +) -> tuple[Path | None, Path | None, ConfigDict, list[str]]: """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 + ignored_files: list[str] = [] for arg in args: argpath = absolutepath(arg) @@ -116,17 +117,17 @@ def locate_config( found_pyproject_toml = p ini_config = load_config_dict_from_file(p) if ini_config is not None: - 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 + index = config_names.index(config_name) + if index < len(config_names) - 1: + for remainder in config_names[index + 1 :]: + p2 = base / remainder + if p2.is_file(): + if load_config_dict_from_file(p2) is not None: + ignored_files.append(remainder) + return base, p, ini_config, ignored_files if found_pyproject_toml is not None: - return found_pyproject_toml.parent, found_pyproject_toml, {}, False - return None, None, {}, False + return found_pyproject_toml.parent, found_pyproject_toml, {}, [] + return None, None, {}, [] def get_common_ancestor( @@ -186,7 +187,7 @@ def determine_setup( args: Sequence[str], rootdir_cmd_arg: str | None, invocation_dir: Path, -) -> tuple[Path, Path | None, ConfigDict, bool]: +) -> tuple[Path, Path | None, ConfigDict, list[str]]: """Determine the rootdir, inifile and ini configuration values from the command line arguments. @@ -201,7 +202,8 @@ def determine_setup( """ rootdir = None dirs = get_dirs_from_args(args) - should_warn = False + ignored_files: list[str] = [] + if inifile: inipath_ = absolutepath(inifile) inipath: Path | None = inipath_ @@ -210,7 +212,7 @@ def determine_setup( rootdir = inipath_.parent else: ancestor = get_common_ancestor(invocation_dir, dirs) - rootdir, inipath, inicfg, should_warn = locate_config( + rootdir, inipath, inicfg, ignored_files = locate_config( invocation_dir, [ancestor] ) if rootdir is None and rootdir_cmd_arg is None: @@ -234,7 +236,7 @@ def determine_setup( f"Directory '{rootdir}' not found. Check your '--rootdir' option." ) assert rootdir is not None - return rootdir, inipath, inicfg or {}, should_warn + return rootdir, inipath, inicfg or {}, ignored_files def is_fs_root(p: Path) -> bool: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index e3a8505039f..8581ed22b2d 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -880,8 +880,8 @@ def pytest_report_header(self, config: Config) -> list[str]: if config.inipath: warning = "" - if config.should_warn: - warning = " (WARNING: ignoring pytest config in pyproject.toml!)" + if config._ignored_files: + warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_files)}!)" result.append( "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 4f9b1b8d776..e6b77ae5546 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2914,6 +2914,38 @@ def test_warning_when_init_trumps_pyproject_toml( ) +def test_warning_when_init_trumps_multiple_files( + 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="") + pytester.makeini( + """ + # tox.ini + [pytest] + minversion = 6.0 + addopts = -ra -q + testpaths = + tests + integration + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml, tox.ini!)", + ] + ) + + def test_no_warning_when_init_but_pyproject_toml_has_no_entry( pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: From 5d561ff4f63b686962415ce5b921923d0477715b Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Wed, 8 Oct 2025 14:22:46 +0200 Subject: [PATCH 5/8] chore: rename ignored_file to ignored_config_file --- src/_pytest/config/__init__.py | 4 ++-- src/_pytest/config/findpaths.py | 12 ++++++------ src/_pytest/terminal.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3b2edf61c77..a34953480de 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1242,7 +1242,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, ignored_files = determine_setup( + rootpath, inipath, inicfg, ignored_config_files = determine_setup( inifile=ns.inifilename, args=ns.file_or_dir + unknown_args, rootdir_cmd_arg=ns.rootdir or None, @@ -1250,7 +1250,7 @@ def _initini(self, args: Sequence[str]) -> None: ) self._rootpath = rootpath self._inipath = inipath - self._ignored_files = ignored_files + self._ignored_config_files = ignored_config_files 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 027a986c5de..1f388d63e1f 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -105,7 +105,7 @@ def locate_config( if not args: args = [invocation_dir] found_pyproject_toml: Path | None = None - ignored_files: list[str] = [] + ignored_config_files: list[str] = [] for arg in args: argpath = absolutepath(arg) @@ -123,8 +123,8 @@ def locate_config( p2 = base / remainder if p2.is_file(): if load_config_dict_from_file(p2) is not None: - ignored_files.append(remainder) - return base, p, ini_config, ignored_files + ignored_config_files.append(remainder) + return base, p, ini_config, ignored_config_files if found_pyproject_toml is not None: return found_pyproject_toml.parent, found_pyproject_toml, {}, [] return None, None, {}, [] @@ -202,7 +202,7 @@ def determine_setup( """ rootdir = None dirs = get_dirs_from_args(args) - ignored_files: list[str] = [] + ignored_config_files: list[str] = [] if inifile: inipath_ = absolutepath(inifile) @@ -212,7 +212,7 @@ def determine_setup( rootdir = inipath_.parent else: ancestor = get_common_ancestor(invocation_dir, dirs) - rootdir, inipath, inicfg, ignored_files = locate_config( + rootdir, inipath, inicfg, ignored_config_files = locate_config( invocation_dir, [ancestor] ) if rootdir is None and rootdir_cmd_arg is None: @@ -236,7 +236,7 @@ def determine_setup( f"Directory '{rootdir}' not found. Check your '--rootdir' option." ) assert rootdir is not None - return rootdir, inipath, inicfg or {}, ignored_files + return rootdir, inipath, inicfg or {}, ignored_config_files def is_fs_root(p: Path) -> bool: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8581ed22b2d..ed62c9e345e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -880,8 +880,8 @@ def pytest_report_header(self, config: Config) -> list[str]: if config.inipath: warning = "" - if config._ignored_files: - warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_files)}!)" + if config._ignored_config_files: + warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)" result.append( "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning ) From a7d797690228d5672d2df2b83f88bd1cc295f142 Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Thu, 9 Oct 2025 12:07:44 +0200 Subject: [PATCH 6/8] chore: apply suggestion - Improved data type - Fixed multiple docstring - Cleaned up remainder config file check loop Co-authored-by: Bruno Oliveira --- changelog/13330.improvement.rst | 2 +- src/_pytest/config/findpaths.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/changelog/13330.improvement.rst b/changelog/13330.improvement.rst index 226b908881d..58350ea30ef 100644 --- a/changelog/13330.improvement.rst +++ b/changelog/13330.improvement.rst @@ -1,3 +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. +Having pytest configuration spread over more than one file (for example having both a ``pytest.ini`` file and ``pyproject.toml`` with a ``[tool.pytest.ini_options]`` table) will now print a warning to make it clearer to the user that only one of them is actually used. -- by :user:`sgaist` diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 1f388d63e1f..f14ae4c67a2 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -91,9 +91,11 @@ def make_scalar(v: object) -> str | list[str]: def locate_config( invocation_dir: Path, args: Iterable[Path], -) -> tuple[Path | None, Path | None, ConfigDict, list[str]]: +) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]: """Search in the list of arguments for a valid ini-file for pytest, - and return a tuple of (rootdir, inifile, cfg-dict).""" + and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where + ignored-config-files is a list of config basenames found that contain + pytest configuration but were ignored.""" config_names = [ "pytest.ini", ".pytest.ini", @@ -118,12 +120,10 @@ def locate_config( ini_config = load_config_dict_from_file(p) if ini_config is not None: index = config_names.index(config_name) - if index < len(config_names) - 1: - for remainder in config_names[index + 1 :]: - p2 = base / remainder - if p2.is_file(): - if load_config_dict_from_file(p2) is not None: - ignored_config_files.append(remainder) + for remainder in config_names[index + 1 :]: + p2 = base / remainder + if p2.is_file() and load_config_dict_from_file(p2) is not None: + ignored_config_files.append(remainder) return base, p, ini_config, ignored_config_files if found_pyproject_toml is not None: return found_pyproject_toml.parent, found_pyproject_toml, {}, [] From 06e1527714414ceb8d6343c3cd901fa747483b70 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:08:04 +0000 Subject: [PATCH 7/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/config/findpaths.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index f14ae4c67a2..6428f085ada 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -93,7 +93,7 @@ def locate_config( args: Iterable[Path], ) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]: """Search in the list of arguments for a valid ini-file for pytest, - and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where + and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where ignored-config-files is a list of config basenames found that contain pytest configuration but were ignored.""" config_names = [ @@ -122,7 +122,10 @@ def locate_config( index = config_names.index(config_name) for remainder in config_names[index + 1 :]: p2 = base / remainder - if p2.is_file() and load_config_dict_from_file(p2) is not None: + if ( + p2.is_file() + and load_config_dict_from_file(p2) is not None + ): ignored_config_files.append(remainder) return base, p, ini_config, ignored_config_files if found_pyproject_toml is not None: From 7f5424f6533432c7932a4abd77360950b9c05b98 Mon Sep 17 00:00:00 2001 From: Samuel Gaist Date: Thu, 9 Oct 2025 12:12:00 +0200 Subject: [PATCH 8/8] chore: fixed mypy error --- src/_pytest/config/findpaths.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 6428f085ada..3f08c8121a9 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -190,7 +190,7 @@ def determine_setup( args: Sequence[str], rootdir_cmd_arg: str | None, invocation_dir: Path, -) -> tuple[Path, Path | None, ConfigDict, list[str]]: +) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]: """Determine the rootdir, inifile and ini configuration values from the command line arguments. @@ -205,7 +205,7 @@ def determine_setup( """ rootdir = None dirs = get_dirs_from_args(args) - ignored_config_files: list[str] = [] + ignored_config_files: Sequence[str] = [] if inifile: inipath_ = absolutepath(inifile)