diff --git a/changelog/14638.bugfix.rst b/changelog/14638.bugfix.rst new file mode 100644 index 00000000000..cc07b54b8b7 --- /dev/null +++ b/changelog/14638.bugfix.rst @@ -0,0 +1 @@ +Top-level options in ``pytest.toml`` and ``.pytest.toml`` files now raise an error instead of being silently ignored when no ``[pytest]`` table is present. diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index e74546c6e28..2a4bed319a9 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -107,13 +107,21 @@ def load_config_dict_from_file( # pytest.toml and .pytest.toml use [pytest] table directly. if filepath.name in ("pytest.toml", ".pytest.toml"): - pytest_config = config.get("pytest", {}) - if pytest_config: + if "pytest" in config: # TOML mode - preserve native TOML types. return { k: ConfigValue(v, origin="file", mode="toml") - for k, v in pytest_config.items() + for k, v in config["pytest"].items() } + top_level_options = [ + key for key, value in config.items() if not isinstance(value, dict) + ] + if top_level_options: + raise UsageError( + f"{filepath}: pytest configuration must be under a " + f"[pytest] table (found top-level options: " + f"{', '.join(top_level_options)})" + ) # "pytest.toml" files are always the source of configuration, even if empty. return {} diff --git a/testing/test_config.py b/testing/test_config.py index 7886610242d..9583c9131fa 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -164,6 +164,28 @@ def test_toml_config_names(self, pytester: Pytester, name: str) -> None: config = pytester.parseconfig() assert config.getini("minversion") == "3.36" + @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) + def test_toml_config_names_without_section_errors( + self, pytester: Pytester, name: str + ) -> None: + config_path = pytester.path.joinpath(name) + config_path.write_text( + textwrap.dedent( + """ + minversion = "3.36" + addopts = ["-v"] + """ + ), + encoding="utf-8", + ) + with pytest.raises(UsageError) as excinfo: + pytester.parseconfig() + assert str(excinfo.value) == ( + f"{config_path}: " + "pytest configuration must be under a [pytest] table " + "(found top-level options: minversion, addopts)" + ) + def test_pyproject_toml(self, pytester: Pytester) -> None: pyproject_toml = pytester.makepyprojecttoml( """