Skip to content

Commit 288d573

Browse files
authored
Merge pull request #13797 from sgaist/add-warning-on-pytest-ini-and-pyproject-presence
feat: add warning when pytest.ini and pyproject.toml are present
1 parent db89537 commit 288d573

File tree

7 files changed

+143
-23
lines changed

7 files changed

+143
-23
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ Sadra Barikbin
403403
Saiprasad Kale
404404
Samuel Colvin
405405
Samuel Dion-Girardeau
406+
Samuel Gaist
406407
Samuel Jirovec
407408
Samuel Searles-Bryant
408409
Samuel Therrien (Avasam)

changelog/13330.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
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.
2+
3+
-- by :user:`sgaist`

src/_pytest/config/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1242,14 +1242,15 @@ def _initini(self, args: Sequence[str]) -> None:
12421242
ns, unknown_args = self._parser.parse_known_and_unknown_args(
12431243
args, namespace=copy.copy(self.option)
12441244
)
1245-
rootpath, inipath, inicfg = determine_setup(
1245+
rootpath, inipath, inicfg, ignored_config_files = determine_setup(
12461246
inifile=ns.inifilename,
12471247
args=ns.file_or_dir + unknown_args,
12481248
rootdir_cmd_arg=ns.rootdir or None,
12491249
invocation_dir=self.invocation_params.dir,
12501250
)
12511251
self._rootpath = rootpath
12521252
self._inipath = inipath
1253+
self._ignored_config_files = ignored_config_files
12531254
self.inicfg = inicfg
12541255
self._parser.extra_info["rootdir"] = str(self.rootpath)
12551256
self._parser.extra_info["inifile"] = str(self.inipath)

src/_pytest/config/findpaths.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ def make_scalar(v: object) -> str | list[str]:
9191
def locate_config(
9292
invocation_dir: Path,
9393
args: Iterable[Path],
94-
) -> tuple[Path | None, Path | None, ConfigDict]:
94+
) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]:
9595
"""Search in the list of arguments for a valid ini-file for pytest,
96-
and return a tuple of (rootdir, inifile, cfg-dict)."""
96+
and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where
97+
ignored-config-files is a list of config basenames found that contain
98+
pytest configuration but were ignored."""
9799
config_names = [
98100
"pytest.ini",
99101
".pytest.ini",
@@ -105,6 +107,8 @@ def locate_config(
105107
if not args:
106108
args = [invocation_dir]
107109
found_pyproject_toml: Path | None = None
110+
ignored_config_files: list[str] = []
111+
108112
for arg in args:
109113
argpath = absolutepath(arg)
110114
for base in (argpath, *argpath.parents):
@@ -115,10 +119,18 @@ def locate_config(
115119
found_pyproject_toml = p
116120
ini_config = load_config_dict_from_file(p)
117121
if ini_config is not None:
118-
return base, p, ini_config
122+
index = config_names.index(config_name)
123+
for remainder in config_names[index + 1 :]:
124+
p2 = base / remainder
125+
if (
126+
p2.is_file()
127+
and load_config_dict_from_file(p2) is not None
128+
):
129+
ignored_config_files.append(remainder)
130+
return base, p, ini_config, ignored_config_files
119131
if found_pyproject_toml is not None:
120-
return found_pyproject_toml.parent, found_pyproject_toml, {}
121-
return None, None, {}
132+
return found_pyproject_toml.parent, found_pyproject_toml, {}, []
133+
return None, None, {}, []
122134

123135

124136
def get_common_ancestor(
@@ -178,7 +190,7 @@ def determine_setup(
178190
args: Sequence[str],
179191
rootdir_cmd_arg: str | None,
180192
invocation_dir: Path,
181-
) -> tuple[Path, Path | None, ConfigDict]:
193+
) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]:
182194
"""Determine the rootdir, inifile and ini configuration values from the
183195
command line arguments.
184196
@@ -193,6 +205,8 @@ def determine_setup(
193205
"""
194206
rootdir = None
195207
dirs = get_dirs_from_args(args)
208+
ignored_config_files: Sequence[str] = []
209+
196210
if inifile:
197211
inipath_ = absolutepath(inifile)
198212
inipath: Path | None = inipath_
@@ -201,15 +215,17 @@ def determine_setup(
201215
rootdir = inipath_.parent
202216
else:
203217
ancestor = get_common_ancestor(invocation_dir, dirs)
204-
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
218+
rootdir, inipath, inicfg, ignored_config_files = locate_config(
219+
invocation_dir, [ancestor]
220+
)
205221
if rootdir is None and rootdir_cmd_arg is None:
206222
for possible_rootdir in (ancestor, *ancestor.parents):
207223
if (possible_rootdir / "setup.py").is_file():
208224
rootdir = possible_rootdir
209225
break
210226
else:
211227
if dirs != [ancestor]:
212-
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
228+
rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs)
213229
if rootdir is None:
214230
rootdir = get_common_ancestor(
215231
invocation_dir, [invocation_dir, ancestor]
@@ -223,7 +239,7 @@ def determine_setup(
223239
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
224240
)
225241
assert rootdir is not None
226-
return rootdir, inipath, inicfg or {}
242+
return rootdir, inipath, inicfg or {}, ignored_config_files
227243

228244

229245
def is_fs_root(p: Path) -> bool:

src/_pytest/terminal.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,7 +879,12 @@ def pytest_report_header(self, config: Config) -> list[str]:
879879
result = [f"rootdir: {config.rootpath}"]
880880

881881
if config.inipath:
882-
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
882+
warning = ""
883+
if config._ignored_config_files:
884+
warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)"
885+
result.append(
886+
"configfile: " + bestrelpath(config.rootpath, config.inipath) + warning
887+
)
883888

884889
if config.args_source == Config.ArgsSource.TESTPATHS:
885890
testpaths: list[str] = config.getini("testpaths")

testing/test_config.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_getcfg_and_config(
5656
),
5757
encoding="utf-8",
5858
)
59-
_, _, cfg = locate_config(Path.cwd(), [sub])
59+
_, _, cfg, _ = locate_config(Path.cwd(), [sub])
6060
assert cfg["name"] == "value"
6161
config = pytester.parseconfigure(str(sub))
6262
assert config.inicfg["name"] == "value"
@@ -1635,15 +1635,15 @@ def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
16351635
b = a / "b"
16361636
b.mkdir()
16371637
for args in ([str(tmp_path)], [str(a)], [str(b)]):
1638-
rootpath, parsed_inipath, _ = determine_setup(
1638+
rootpath, parsed_inipath, *_ = determine_setup(
16391639
inifile=None,
16401640
args=args,
16411641
rootdir_cmd_arg=None,
16421642
invocation_dir=Path.cwd(),
16431643
)
16441644
assert rootpath == tmp_path
16451645
assert parsed_inipath == inipath
1646-
rootpath, parsed_inipath, ini_config = determine_setup(
1646+
rootpath, parsed_inipath, ini_config, _ = determine_setup(
16471647
inifile=None,
16481648
args=[str(b), str(a)],
16491649
rootdir_cmd_arg=None,
@@ -1660,7 +1660,7 @@ def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> Non
16601660
a = tmp_path / "a"
16611661
a.mkdir()
16621662
(a / name).touch()
1663-
rootpath, parsed_inipath, _ = determine_setup(
1663+
rootpath, parsed_inipath, *_ = determine_setup(
16641664
inifile=None,
16651665
args=[str(a)],
16661666
rootdir_cmd_arg=None,
@@ -1674,7 +1674,7 @@ def test_setuppy_fallback(self, tmp_path: Path) -> None:
16741674
a.mkdir()
16751675
(a / "setup.cfg").touch()
16761676
(tmp_path / "setup.py").touch()
1677-
rootpath, inipath, inicfg = determine_setup(
1677+
rootpath, inipath, inicfg, _ = determine_setup(
16781678
inifile=None,
16791679
args=[str(a)],
16801680
rootdir_cmd_arg=None,
@@ -1686,7 +1686,7 @@ def test_setuppy_fallback(self, tmp_path: Path) -> None:
16861686

16871687
def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
16881688
monkeypatch.chdir(tmp_path)
1689-
rootpath, inipath, inicfg = determine_setup(
1689+
rootpath, inipath, inicfg, _ = determine_setup(
16901690
inifile=None,
16911691
args=[str(tmp_path)],
16921692
rootdir_cmd_arg=None,
@@ -1713,7 +1713,7 @@ def test_with_specific_inifile(
17131713
p = tmp_path / name
17141714
p.touch()
17151715
p.write_text(contents, encoding="utf-8")
1716-
rootpath, inipath, ini_config = determine_setup(
1716+
rootpath, inipath, ini_config, _ = determine_setup(
17171717
inifile=str(p),
17181718
args=[str(tmp_path)],
17191719
rootdir_cmd_arg=None,
@@ -1761,7 +1761,7 @@ def test_with_arg_outside_cwd_without_inifile(
17611761
a.mkdir()
17621762
b = tmp_path / "b"
17631763
b.mkdir()
1764-
rootpath, inifile, _ = determine_setup(
1764+
rootpath, inifile, *_ = determine_setup(
17651765
inifile=None,
17661766
args=[str(a), str(b)],
17671767
rootdir_cmd_arg=None,
@@ -1777,7 +1777,7 @@ def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None:
17771777
b.mkdir()
17781778
inipath = a / "pytest.ini"
17791779
inipath.touch()
1780-
rootpath, parsed_inipath, _ = determine_setup(
1780+
rootpath, parsed_inipath, *_ = determine_setup(
17811781
inifile=None,
17821782
args=[str(a), str(b)],
17831783
rootdir_cmd_arg=None,
@@ -1791,7 +1791,7 @@ def test_with_non_dir_arg(
17911791
self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch
17921792
) -> None:
17931793
monkeypatch.chdir(tmp_path)
1794-
rootpath, inipath, _ = determine_setup(
1794+
rootpath, inipath, *_ = determine_setup(
17951795
inifile=None,
17961796
args=dirs,
17971797
rootdir_cmd_arg=None,
@@ -1807,7 +1807,7 @@ def test_with_existing_file_in_subdir(
18071807
a.mkdir()
18081808
(a / "exists").touch()
18091809
monkeypatch.chdir(tmp_path)
1810-
rootpath, inipath, _ = determine_setup(
1810+
rootpath, inipath, *_ = determine_setup(
18111811
inifile=None,
18121812
args=["a/exist"],
18131813
rootdir_cmd_arg=None,
@@ -1826,7 +1826,7 @@ def test_with_config_also_in_parent_directory(
18261826
(tmp_path / "myproject" / "tests").mkdir()
18271827
monkeypatch.chdir(tmp_path / "myproject")
18281828

1829-
rootpath, inipath, _ = determine_setup(
1829+
rootpath, inipath, *_ = determine_setup(
18301830
inifile=None,
18311831
args=["tests/"],
18321832
rootdir_cmd_arg=None,

testing/test_terminal.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,6 +2893,100 @@ def test_format_trimmed() -> None:
28932893
assert _format_trimmed(" ({}) ", msg, len(msg) + 3) == " (unconditional ...) "
28942894

28952895

2896+
def test_warning_when_init_trumps_pyproject_toml(
2897+
pytester: Pytester, monkeypatch: MonkeyPatch
2898+
) -> None:
2899+
"""Regression test for #7814."""
2900+
tests = pytester.path.joinpath("tests")
2901+
tests.mkdir()
2902+
pytester.makepyprojecttoml(
2903+
f"""
2904+
[tool.pytest.ini_options]
2905+
testpaths = ['{tests}']
2906+
"""
2907+
)
2908+
pytester.makefile(".ini", pytest="")
2909+
result = pytester.runpytest()
2910+
result.stdout.fnmatch_lines(
2911+
[
2912+
"configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)",
2913+
]
2914+
)
2915+
2916+
2917+
def test_warning_when_init_trumps_multiple_files(
2918+
pytester: Pytester, monkeypatch: MonkeyPatch
2919+
) -> None:
2920+
"""Regression test for #7814."""
2921+
tests = pytester.path.joinpath("tests")
2922+
tests.mkdir()
2923+
pytester.makepyprojecttoml(
2924+
f"""
2925+
[tool.pytest.ini_options]
2926+
testpaths = ['{tests}']
2927+
"""
2928+
)
2929+
pytester.makefile(".ini", pytest="")
2930+
pytester.makeini(
2931+
"""
2932+
# tox.ini
2933+
[pytest]
2934+
minversion = 6.0
2935+
addopts = -ra -q
2936+
testpaths =
2937+
tests
2938+
integration
2939+
"""
2940+
)
2941+
result = pytester.runpytest()
2942+
result.stdout.fnmatch_lines(
2943+
[
2944+
"configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml, tox.ini!)",
2945+
]
2946+
)
2947+
2948+
2949+
def test_no_warning_when_init_but_pyproject_toml_has_no_entry(
2950+
pytester: Pytester, monkeypatch: MonkeyPatch
2951+
) -> None:
2952+
"""Regression test for #7814."""
2953+
tests = pytester.path.joinpath("tests")
2954+
tests.mkdir()
2955+
pytester.makepyprojecttoml(
2956+
f"""
2957+
[tool]
2958+
testpaths = ['{tests}']
2959+
"""
2960+
)
2961+
pytester.makefile(".ini", pytest="")
2962+
result = pytester.runpytest()
2963+
result.stdout.fnmatch_lines(
2964+
[
2965+
"configfile: pytest.ini",
2966+
]
2967+
)
2968+
2969+
2970+
def test_no_warning_on_terminal_with_a_single_config_file(
2971+
pytester: Pytester, monkeypatch: MonkeyPatch
2972+
) -> None:
2973+
"""Regression test for #7814."""
2974+
tests = pytester.path.joinpath("tests")
2975+
tests.mkdir()
2976+
pytester.makepyprojecttoml(
2977+
f"""
2978+
[tool.pytest.ini_options]
2979+
testpaths = ['{tests}']
2980+
"""
2981+
)
2982+
result = pytester.runpytest()
2983+
result.stdout.fnmatch_lines(
2984+
[
2985+
"configfile: pyproject.toml",
2986+
]
2987+
)
2988+
2989+
28962990
class TestFineGrainedTestCase:
28972991
DEFAULT_FILE_CONTENTS = """
28982992
import pytest

0 commit comments

Comments
 (0)