diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index e74546c6e28..9677cc65a06 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -106,8 +106,12 @@ def load_config_dict_from_file( raise UsageError(f"{filepath}: {exc}") from exc # pytest.toml and .pytest.toml use [pytest] table directly. + # Top-level non-table keys are also treated as pytest config, + # since the filename already identifies the context and TOML + # sections are optional. if filepath.name in ("pytest.toml", ".pytest.toml"): - pytest_config = config.get("pytest", {}) + top_config = {k: v for k, v in config.items() if not isinstance(v, dict)} + pytest_config = {**top_config, **config.get("pytest", {})} if pytest_config: # TOML mode - preserve native TOML types. return { diff --git a/testing/test_config.py b/testing/test_config.py index 7886610242d..22f3ee6540d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -217,6 +217,20 @@ def test_empty_pytest_toml(self, pytester: Pytester, name: str) -> None: config = pytester.parseconfig() assert config.inipath == pytest_toml + @pytest.mark.parametrize("name", ["pytest.toml", ".pytest.toml"]) + def test_headerless_pytest_toml(self, pytester: Pytester, name: str) -> None: + """Top-level keys in pytest.toml are treated as pytest config (#14638).""" + pytester.path.joinpath(name).write_text( + textwrap.dedent( + """ + minversion = "9.0" + """ + ), + encoding="utf-8", + ) + config = pytester.parseconfig() + assert config.getini("minversion") == "9.0" + def test_pytest_toml_trumps_pyproject_toml(self, pytester: Pytester) -> None: """A pytest.toml always takes precedence over a pyproject.toml file.""" pytester.makepyprojecttoml(