From 78697caacc9caf1eb8ab85774951fe9fc7fc9b46 Mon Sep 17 00:00:00 2001 From: Shurong Cao <170531907+CAOShurong@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:48:36 +0800 Subject: [PATCH] Normalize paths when matching manifest entries to package dirs in build_py Manifest/SOURCES.txt paths use forward slashes while get_package_dir emits OS-native separators; on Windows the mismatch made analyze_manifest overshoot the package dir, emitting a false 'Package is absent from the packages configuration' warning and misclassifying extension sources (e.g. .pyx files written in the documented Unix form) as parent-package data. Normalize both sides before comparison. Fixes #5093 --- newsfragments/5093.bugfix.rst | 5 +++ setuptools/command/build_py.py | 6 ++- setuptools/tests/test_build_py.py | 66 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5093.bugfix.rst diff --git a/newsfragments/5093.bugfix.rst b/newsfragments/5093.bugfix.rst new file mode 100644 index 0000000000..9d0f76b385 --- /dev/null +++ b/newsfragments/5093.bugfix.rst @@ -0,0 +1,5 @@ +Fixed a Windows-only false ``Package '...' is absent from the `packages` +configuration`` warning (and the resulting misclassification of the file as +package data) that occurred when an ``Extension`` source path written with +forward slashes (the documented, portable form) lived inside a subpackage: +manifest paths and package directories are now normalized before comparison. diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py index 3c7c2d1bd6..b3dac06c2c 100644 --- a/setuptools/command/build_py.py +++ b/setuptools/command/build_py.py @@ -181,7 +181,9 @@ def analyze_manifest(self) -> None: src_dirs: dict[str, str] = {} for package in self.packages or (): # Locate package source directory - src_dirs[assert_relative(self.get_package_dir(package))] = package + src_dirs[ + os.path.normpath(assert_relative(self.get_package_dir(package))) + ] = package if ( self.existing_egg_info_dir @@ -198,7 +200,7 @@ def analyze_manifest(self) -> None: check = _IncludePackageDataAbuse() for path in self._filter_build_files(files, egg_info_dir): - d, f = os.path.split(assert_relative(path)) + d, f = os.path.split(assert_relative(os.path.normpath(path))) prev = None oldf = f while d and d != prev and d not in src_dirs: diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py index 62ed408f3c..fd7816243c 100644 --- a/setuptools/tests/test_build_py.py +++ b/setuptools/tests/test_build_py.py @@ -479,3 +479,69 @@ def get_outputs(build_py): os.path.relpath(x, build_dir).replace(os.sep, "/") for x in build_py.get_outputs() } + + +def test_analyze_manifest_mixed_separators(tmpdir_cwd): + """ + A subpackage source path written with forward slashes (the documented, + portable form for ``Extension`` sources) must not be misclassified as + package data of the parent package on Windows due to a separator mismatch + between manifest paths and package dirs. + + Regression test for #5093. + """ + jaraco.path.build({ + "setup.cfg": DALS( + """ + [metadata] + name = mypkg + version = 42 + + [options] + include_package_data = True + packages = + mypkg + mypkg.lib + """ + ), + "mypkg": { + "__init__.py": "", + "lib": {"__init__.py": "", "say.pyx": "# dummy"}, + }, + "MANIFEST.in": DALS( + """ + recursive-include mypkg *.pyx + prune dist + prune build + prune *.egg-info + """ + ), + }) + from setuptools.extension import Extension + + dist = Distribution({"script_name": "%PEP 517%"}) + dist.parse_config_files() + dist.ext_modules = [Extension("mypkg.lib.say", ["mypkg/lib/say.pyx"])] + + egg_info = dist.get_command_obj("egg_info") + dist.run_command("egg_info") + + build_py = dist.get_command_obj("build_py") + build_py.finalize_options() + build_py.existing_egg_info_dir = egg_info.egg_info + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + build_py.analyze_manifest() + + offending = [ + w + for w in caught + if issubclass(w.category, SetuptoolsDeprecationWarning) + and "mypkg.lib" in str(w.message) + and "absent" in str(w.message) + ] + assert not offending, ( + "Subpackage whose extension source is written in Unix form was " + "misclassified as data of the parent package (mixed path separators)" + )