Skip to content

Respect excluded subpackages in build_py - #5248

Open
itscloud0 wants to merge 10 commits into
pypa:mainfrom
itscloud0:fix-3260-excluded-subpackages
Open

Respect excluded subpackages in build_py#5248
itscloud0 wants to merge 10 commits into
pypa:mainfrom
itscloud0:fix-3260-excluded-subpackages

Conversation

@itscloud0

Copy link
Copy Markdown
Contributor

Summary of changes

Fix packages.find.exclude so excluded nested Python subpackages are not copied into wheels through include_package_data=True.

Closes #3260.

Problem

On current main, a project configured with:

  • include_package_data = True
  • packages = find:
  • exclude = *.tests*

still ships mypkg/tests/* in the built wheel when those files are pulled in through MANIFEST.in.

The repo already had an xfailed regression for this case in setuptools/tests/test_build_py.py, and a direct repro on current main still produced a wheel containing mypkg/tests/test_mypkg.py and mypkg/tests/test_file.txt.

Solution

When build_py walks manifest entries, stop promoting files from nested directories that already contain Python sources into the parent package's data files. That keeps explicitly excluded subpackages out of the wheel instead of reintroducing them through include_package_data.

The existing regression now asserts the intended behavior directly instead of warning and xfail-ing.

Verification

  • uv run --extra test --extra cover pytest setuptools/tests/test_build_py.py -q
  • uvx ruff check setuptools/command/build_py.py setuptools/tests/test_build_py.py
  • uvx ruff format --check setuptools/command/build_py.py setuptools/tests/test_build_py.py
  • Reproduced the issue's toy mypkg/tests wheel build on main, then reran it after the patch and confirmed the wheel no longer contains mypkg/tests/*.

@itscloud0
itscloud0 marked this pull request as ready for review July 4, 2026 17:10
@mergify

mergify Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@abravalheri abravalheri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @itscloud0, thank you very much for the contribution.

I think I understand the motivation behind the PR. However, I think we should add another test to cover what I consider to be an interesting edge case.

Would it be possible to add a test along the following lines?

pyproject.toml
MANIFEST.in
src/
└── mypkg/
    ├── __init__.py
    ├── data.txt
    └── subpkg/
        ├── sample1.json
        └── sample2.json

with:

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
where = ["src"]
exclude = ["mypkg.subpkg", "mypkg.subpkg.*"]

and a MANIFEST.in that includes the contents of mypkg/subpkg (for the sake of the sdist).

My understanding of the reasoning behind this PR is that an explicitly excluded nested package should not be reintroduced into the final wheel via include-package-data = true.

If that interpretation is correct, I wouldn't expect the files under mypkg/subpkg/ to appear in the wheel.

I realise there are different views on whether directories without Python files should be considered packages. Personally, I don't make any differentiation (which is consistent with the Python interpreter behaviour). But, for this example in particular, the distinction should be irrelevant since the directory is explicitly excluded (so the user expectation would be for the directory to not be present in the final wheel).

for part in candidate.relative_to(directory).with_suffix("").parts
)
for candidate in Path(directory).glob("**/*.py")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am a bit concerned about the amount of nested iteration introduced by this change.

_find_package_data_owner is called for each file discovered by the _filter_build_files function.

In turn, _find_package_data_owner contains a while loop.

Inside this loop, _contains_python_sources is called.

Inside _contains_python_sources, we have another two levels of iteration.

It is fine that while directory and directory != previous and directory not in src_dirs: is essentially the same as in the original code, but the addition of these extra loops appears to increase the overall complexity, specially because Path.glob("**/*.py") may repeatly invoke recursive filesystem scans.

Maybe this will impact performance on larger source trees...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] options.packages.find.exclude not taking effect when include_package_data = True

3 participants