Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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`
11 changes: 10 additions & 1 deletion src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,14 @@ def inipath(self) -> pathlib.Path | None:
"""
return self._inipath

@property
def should_warn(self) -> bool:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update.

  • We shouldn't expose this in the public API, let's keep it private unless there's a need.

  • If it's private, no need for a property

  • The name should_warn is too generic, we should say warn about what. Or better, something like _ignored_config_files = ["pyproject.toml", ".pytest.ini'] containing the ignored files we detected. Semantically whether or not to warn is up to the place which warns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me, I'll update the code.

"""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).
Expand Down Expand Up @@ -1242,14 +1250,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, should_warn = 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._should_warn = should_warn
self.inicfg = inicfg
self._parser.extra_info["rootdir"] = str(self.rootpath)
self._parser.extra_info["inifile"] = str(self.inipath)
Expand Down
27 changes: 19 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, 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 = [
Expand All @@ -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):
Expand All @@ -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(
Expand Down Expand Up @@ -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.

Expand All @@ -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_
Expand All @@ -201,15 +210,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, 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():
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 +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:
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.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")
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
62 changes: 62 additions & 0 deletions testing/test_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down