diff --git a/src/_pytest/main.py b/src/_pytest/main.py index fc9cf696e9d..1ffe8506164 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -12,6 +12,7 @@ from pathlib import Path import sys from typing import AbstractSet +from typing import Any from typing import Callable from typing import Dict from typing import final @@ -365,6 +366,20 @@ def pytest_runtestloop(session: Session) -> bool: return True +def _decode_toml_file(toml: Path) -> dict[str, Any] | None: + """Attempt to decode a toml file into a dict, returning None if fails.""" + if sys.version_info >= (3, 11): + import tomllib + else: + import tomli as tomllib + + try: + toml_text = toml.read_text(encoding="utf-8") + return tomllib.loads(toml_text) + except tomllib.TOMLDecodeError: + return None + + def _in_build(path: Path) -> bool: """Attempt to detect if ``path`` is the root of a buildsystem's artifacts by checking known dirnames patterns, and the presence of configuration in @@ -374,9 +389,21 @@ def _in_build(path: Path) -> bool: return False if any(fnmatch_ex(pat, path) for pat in ("build", "dist")): - indicators = ("setup.py", "setup.cfg", "pyproject.toml") - if any((path.parent / f).is_file() for f in indicators): - return True + setup_cfg = path.parent / "setup.cfg" + if (setup_cfg).is_file(): + setup_py = path.parent / "setup.py" + if setup_py.is_file(): + return True + + pyproject_toml = path.parent / "pyproject.toml" + if pyproject_toml.is_file(): + config = _decode_toml_file(pyproject_toml) + if config: + if any( + "setuptools" in cfg + for cfg in config.get("build-system", {}).get("requires", {}) + ): + return True return False diff --git a/testing/test_collection.py b/testing/test_collection.py index 3ff2af3b6f8..9713afeee99 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -275,14 +275,10 @@ def test_missing_permissions_on_unselected_directory_doesnt_crash( result.assert_outcomes(passed=1) known_build_dirs = pytest.mark.parametrize("build_dir", ["build", "dist"]) - known_buildsystem_env = pytest.mark.parametrize( - "buildsystem_indicator_file", ["setup.py", "setup.cfg", "pyproject.toml"] - ) @known_build_dirs - @known_buildsystem_env - def test_build_dirs_collected( - self, pytester: Pytester, build_dir: str, buildsystem_indicator_file: str + def test_build_dirs_collected_when_setuptools_setup_py_present( + self, pytester: Pytester, build_dir: str ) -> None: tmp_path = pytester.path ensure_file(tmp_path / build_dir / "test_module.py").write_text( @@ -292,13 +288,17 @@ def test_build_dirs_collected( result = pytester.runpytest("--collect-only").stdout.str() assert "test_module" in result - ensure_file(tmp_path / buildsystem_indicator_file) + ensure_file(tmp_path / "setup.cfg") + + result = pytester.runpytest("--collect-only").stdout.str() + assert "test_module" in result + ensure_file(tmp_path / "setup.py") result = pytester.runpytest("--collect-only").stdout.str() assert "test_module" not in result @known_build_dirs - def test_build_dirs_collected_when_setuptools_configuration_present( + def test_build_dirs_collected_when_setuptools_present_in_pyproject_toml( self, pytester: Pytester, build_dir: str ) -> None: tmp_path = pytester.path @@ -314,7 +314,11 @@ def test_build_dirs_collected_when_setuptools_configuration_present( result = pytester.runpytest("--collect-only").stdout.str() assert "test_module" in result - ensure_file(tmp_path / "setup.py") + ensure_file(tmp_path / "pyproject.toml").write_text( + '[build-system]\nrequires = ["setuptools", "setuptools-scm"]\n', + encoding="utf-8", + ) + result = pytester.runpytest("--collect-only").stdout.str() assert "test_module" not in result