Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditionally ignore collection of setuptools dirnames #12918

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions changelog/12625.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Conditionally ignore collection of setuptools artifacts dirnames only if the
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Conditionally ignore collection of setuptools artifacts dirnames only if the
Conditionally ignore collection of ``setuptools`` artifacts directory names (``build`` and ``dist``) only if the

directories reside inside a setuptools project, i.e. `setup.cfg` is present alongside
a `setup.py` or a `pyproject.toml` with setuptools configuration inside.
You can use the new `--collect-in-build` to force collection of ignored directories.
7 changes: 6 additions & 1 deletion doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1754,7 +1754,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[seq] matches any character in seq
[!seq] matches any char not in seq

Default patterns are ``'*.egg'``, ``'.*'``, ``'_darcs'``, ``'build'``,
Default patterns are ``'*.egg'``, ``'.*'``, ``'_darcs'``,
``'CVS'``, ``'dist'``, ``'node_modules'``, ``'venv'``, ``'{arch}'``.
Setting a ``norecursedirs`` replaces the default. Here is an example of
how to avoid certain directories:
Expand All @@ -1776,6 +1776,9 @@ passed multiple times. The expected format is ``name=value``. For example::
*must* override ``norecursedirs`` in addition to using the
``--collect-in-virtualenv`` flag.

Similarly, pytest will attempt to intelligently identify and igmore build
artifacts of a setuptools project unless ``--collect-in-build`` is used.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
artifacts of a setuptools project unless ``--collect-in-build`` is used.
artifacts of a ``setuptools`` project unless ``--collect-in-build`` is used.



.. confval:: python_classes

Expand Down Expand Up @@ -2163,6 +2166,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
--keep-duplicates Keep duplicate tests
--collect-in-virtualenv
Don't ignore tests in a local virtualenv directory
--collect-in-build
Don't ignore tests in a local build directory
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Don't ignore tests in a local build directory
Don't ignore tests in a local build directories (build/ and dist/)

--import-mode={prepend,append,importlib}
Prepend/append to sys.path when importing test
modules and conftest files. Default: prepend.
Expand Down
59 changes: 56 additions & 3 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,13 @@
def pytest_addoption(parser: Parser) -> None:
parser.addini(
"norecursedirs",
"Directory patterns to avoid for recursion",
"Directory patterns to avoid collecting (defaults to ignore build artifacts)",
type="args",
default=[
"*.egg",
".*",
"_darcs",
"build",
Copy link
Member

Choose a reason for hiding this comment

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

I think that the description string above should mention that these are still conditionally excluded by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added

"CVS",
"dist",
"node_modules",
"venv",
"{arch}",
Expand Down Expand Up @@ -218,6 +216,13 @@ def pytest_addoption(parser: Parser) -> None:
default=False,
help="Don't ignore tests in a local virtualenv directory",
)
group.addoption(
"--collect-in-build",
action="store_true",
dest="collect_in_build",
default=False,
help="Don't ignore builds in a local build/dist artifacts directory",
)
group.addoption(
"--import-mode",
default="prepend",
Expand Down Expand Up @@ -371,6 +376,50 @@ def pytest_runtestloop(session: Session) -> bool:
return True


def _is_setuptools_in_pyproject_toml(toml: Path) -> bool:
"""Attempt to decode a toml file into a dict, returning None if fails."""
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"""Attempt to decode a toml file into a dict, returning None if fails."""
"""Checks if the given pyproject.toml configures setuptools as the build-system."""

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib

try:
toml_text = toml.read_text(encoding="utf-8")
parsed_toml = tomllib.loads(toml_text)
build_system = parsed_toml.get("build-system", {}).get("requires")
if "setuptools" in build_system:
return True
except tomllib.TOMLDecodeError:
# Not a valid Toml, so no setuptools.
pass

return False


def _in_build(path: Path) -> bool:
"""Detect if ``path`` is the root of a buildsystem's artifacts
by checking known dirnames patterns, and the presence of configuration in
the parent dir by checking for a setup.py, setup.cfg, or pyproject.toml.
"""
if not path.is_dir():
return False

setup_cfg = path.parent / "setup.cfg"
if not setup_cfg.is_file():
return False

if any(fnmatch_ex(pat, path) for pat in ("build", "dist")):
setup_py = path.parent / "setup.py"
if setup_py.is_file():
return True

toml = path.parent / "pyproject.toml"
if toml.is_file() and _is_setuptools_in_pyproject_toml(toml):
return True

return False


def _in_venv(path: Path) -> bool:
"""Attempt to detect if ``path`` is the root of a Virtual Environment by
checking for the existence of the pyvenv.cfg file.
Expand Down Expand Up @@ -422,6 +471,10 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
if not allow_in_venv and _in_venv(collection_path):
return True

allow_in_build = config.getoption("collect_in_build")
if not allow_in_build and _in_build(collection_path):
return True

if collection_path.is_dir():
norecursepatterns = config.getini("norecursedirs")
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
Expand Down
55 changes: 53 additions & 2 deletions testing/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,6 @@ def test_foo():
class TestCollectFS:
def test_ignored_certain_directories(self, pytester: Pytester) -> None:
sus-pe marked this conversation as resolved.
Show resolved Hide resolved
sus-pe marked this conversation as resolved.
Show resolved Hide resolved
tmp_path = pytester.path
ensure_file(tmp_path / "build" / "test_notfound.py")
ensure_file(tmp_path / "dist" / "test_notfound.py")
ensure_file(tmp_path / "_darcs" / "test_notfound.py")
ensure_file(tmp_path / "CVS" / "test_notfound.py")
ensure_file(tmp_path / "{arch}" / "test_notfound.py")
Expand Down Expand Up @@ -276,6 +274,59 @@ def test_missing_permissions_on_unselected_directory_doesnt_crash(
assert result.ret == ExitCode.OK
result.assert_outcomes(passed=1)

known_build_dirs = pytest.mark.parametrize("build_dir", ["build", "dist"])

@known_build_dirs
def test_build_dirs_ignored_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(
"def test_hello(): pass", encoding="utf-8"
)

result = pytester.runpytest("--collect-only").stdout.str()
assert "test_module" in result

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

result = pytester.runpytest("--collect-only", "--collect-in-build").stdout.str()
assert "test_module" in result

@known_build_dirs
def test_build_dirs_ignored_when_setuptools_present_in_pyproject_toml(
self, pytester: Pytester, build_dir: str
) -> None:
tmp_path = pytester.path
ensure_file(tmp_path / build_dir / "test_module.py").write_text(
"def test_hello(): pass", encoding="utf-8"
)

result = pytester.runpytest("--collect-only").stdout.str()
assert "test_module" in result

ensure_file(tmp_path / "setup.cfg")

result = pytester.runpytest("--collect-only").stdout.str()
assert "test_module" in result

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

result = pytester.runpytest("--collect-only", "--collect-in-build").stdout.str()
assert "test_module" in result


class TestCollectPluginHookRelay:
def test_pytest_collect_file(self, pytester: Pytester) -> None:
Expand Down
Loading