Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ Sadra Barikbin
Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau
Samuel Gaist
Samuel Jirovec
Samuel Searles-Bryant
Samuel Therrien (Avasam)
Expand Down
3 changes: 3 additions & 0 deletions changelog/13330.improvement.rst
Original file line number Diff line number Diff line change
@@ -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`
3 changes: 2 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,14 +1242,15 @@ 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, ignored_config_files = determine_setup(
inifile=ns.inifilename,
args=ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir,
)
self._rootpath = rootpath
self._inipath = inipath
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)
Expand Down
29 changes: 21 additions & 8 deletions src/_pytest/config/findpaths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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 = [
Expand All @@ -105,6 +105,8 @@ def locate_config(
if not args:
args = [invocation_dir]
found_pyproject_toml: Path | None = None
ignored_config_files: list[str] = []

for arg in args:
argpath = absolutepath(arg)
for base in (argpath, *argpath.parents):
Expand All @@ -115,10 +117,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
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)
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, {}
return found_pyproject_toml.parent, found_pyproject_toml, {}, []
return None, None, {}, []


def get_common_ancestor(
Expand Down Expand Up @@ -178,7 +187,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, list[str]]:
"""Determine the rootdir, inifile and ini configuration values from the
command line arguments.

Expand All @@ -193,6 +202,8 @@ def determine_setup(
"""
rootdir = None
dirs = get_dirs_from_args(args)
ignored_config_files: list[str] = []

if inifile:
inipath_ = absolutepath(inifile)
inipath: Path | None = inipath_
Expand All @@ -201,15 +212,17 @@ 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, ignored_config_files = 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():
rootdir = possible_rootdir
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]
Expand All @@ -223,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 {}
return rootdir, inipath, inicfg or {}, ignored_config_files


def is_fs_root(p: Path) -> bool:
Expand Down
7 changes: 6 additions & 1 deletion src/_pytest/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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._ignored_config_files:
warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)"
result.append(
"configfile: " + bestrelpath(config.rootpath, config.inipath) + warning
)

if config.args_source == Config.ArgsSource.TESTPATHS:
testpaths: list[str] = config.getini("testpaths")
Expand Down
24 changes: 12 additions & 12 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1635,15 +1635,15 @@ 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,
invocation_dir=Path.cwd(),
)
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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
94 changes: 94 additions & 0 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2893,6 +2893,100 @@ 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_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:
"""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
Expand Down