This commit is contained in:
lz_db
2026-10-16 22:48:30 +08:00
parent 190de72571
commit 0b36fd3383
3161 changed files with 12787 additions and 77274 deletions
@@ -1,29 +1,23 @@
from __future__ import annotations
import fnmatch
import itertools
import os
import stat
import textwrap
from collections.abc import Iterable, Iterator
from functools import partial
from glob import glob
from pathlib import Path
from more_itertools import unique_everseen
from .._path import StrPath, StrPathT
from ..dist import Distribution
from ..warnings import SetuptoolsDeprecationWarning
import distutils.command.build_py as orig
import distutils.errors
from distutils.util import convert_path
import distutils.command.build_py as orig
import os
import fnmatch
import textwrap
import io
import distutils.errors
import itertools
import stat
import warnings
from pathlib import Path
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
_IMPLICIT_DATA_FILES = ('*.pyi', 'py.typed')
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
from setuptools.extern.more_itertools import unique_everseen
def make_writable(target) -> None:
def make_writable(target):
os.chmod(target, os.stat(target).st_mode | stat.S_IWRITE)
@@ -36,10 +30,8 @@ class build_py(orig.build_py):
Also, this version of the 'build_py' command allows you to specify both
'py_modules' and 'packages' in the same setup operation.
"""
distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution
editable_mode: bool = False
existing_egg_info_dir: StrPath | None = None #: Private API, internal use only.
existing_egg_info_dir: Optional[str] = None #: Private API, internal use only.
def finalize_options(self):
orig.build_py.finalize_options(self)
@@ -47,25 +39,18 @@ class build_py(orig.build_py):
self.exclude_package_data = self.distribution.exclude_package_data or {}
if 'data_files' in self.__dict__:
del self.__dict__['data_files']
self.__updated_files = []
def copy_file( # type: ignore[override] # No overload, no bytes support
self,
infile: StrPath,
outfile: StrPathT,
preserve_mode: bool = True,
preserve_times: bool = True,
link: str | None = None,
level: object = 1,
) -> tuple[StrPathT | str, bool]:
def copy_file(self, infile, outfile, preserve_mode=1, preserve_times=1,
link=None, level=1):
# Overwrite base class to allow using links
if link:
infile = str(Path(infile).resolve())
outfile = str(Path(outfile).resolve()) # type: ignore[assignment] # Re-assigning a str when outfile is StrPath is ok
return super().copy_file( # pyright: ignore[reportReturnType] # pypa/distutils#309
infile, outfile, preserve_mode, preserve_times, link, level
)
outfile = str(Path(outfile).resolve())
return super().copy_file(infile, outfile, preserve_mode, preserve_times,
link, level)
def run(self) -> None:
def run(self):
"""Build modules, packages, and copy data files to build directory"""
if not (self.py_modules or self.packages) or self.editable_mode:
return
@@ -79,15 +64,21 @@ class build_py(orig.build_py):
# Only compile actual .py files, using our base class' idea of what our
# output files are.
self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=False))
self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))
def __getattr__(self, attr: str):
def __getattr__(self, attr):
"lazily compute data files"
if attr == 'data_files':
self.data_files = self._get_data_files()
return self.data_files
return orig.build_py.__getattr__(self, attr)
def build_module(self, module, module_file, package):
outfile, copied = orig.build_py.build_module(self, module, module_file, package)
if copied:
self.__updated_files.append(outfile)
return outfile, copied
def _get_data_files(self):
"""Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
self.analyze_manifest()
@@ -123,7 +114,6 @@ class build_py(orig.build_py):
self.package_data,
package,
src_dir,
extra_patterns=_IMPLICIT_DATA_FILES,
)
globs_expanded = map(partial(glob, recursive=True), patterns)
# flatten the expanded globs into an iterable of matches
@@ -135,13 +125,13 @@ class build_py(orig.build_py):
)
return self.exclude_data_files(package, src_dir, files)
def get_outputs(self, include_bytecode: bool = True) -> list[str]: # type: ignore[override] # Using a real boolean instead of 0|1
def get_outputs(self, include_bytecode=1) -> List[str]:
"""See :class:`setuptools.commands.build.SubCommand`"""
if self.editable_mode:
return list(self.get_output_mapping().keys())
return super().get_outputs(include_bytecode)
def get_output_mapping(self) -> dict[str, str]:
def get_output_mapping(self) -> Dict[str, str]:
"""See :class:`setuptools.commands.build.SubCommand`"""
mapping = itertools.chain(
self._get_package_data_output_mapping(),
@@ -149,14 +139,14 @@ class build_py(orig.build_py):
)
return dict(sorted(mapping, key=lambda x: x[0]))
def _get_module_mapping(self) -> Iterator[tuple[str, str]]:
def _get_module_mapping(self) -> Iterator[Tuple[str, str]]:
"""Iterate over all modules producing (dest, src) pairs."""
for package, module, module_file in self.find_all_modules():
for (package, module, module_file) in self.find_all_modules():
package = package.split('.')
filename = self.get_module_outfile(self.build_lib, package, module)
yield (filename, module_file)
def _get_package_data_output_mapping(self) -> Iterator[tuple[str, str]]:
def _get_package_data_output_mapping(self) -> Iterator[Tuple[str, str]]:
"""Iterate over package data producing (dest, src) pairs."""
for package, src_dir, build_dir, filenames in self.data_files:
for filename in filenames:
@@ -164,24 +154,24 @@ class build_py(orig.build_py):
srcfile = os.path.join(src_dir, filename)
yield (target, srcfile)
def build_package_data(self) -> None:
def build_package_data(self):
"""Copy data files into build directory"""
for target, srcfile in self._get_package_data_output_mapping():
self.mkpath(os.path.dirname(target))
_outf, _copied = self.copy_file(srcfile, target)
make_writable(target)
def analyze_manifest(self) -> None:
self.manifest_files: dict[str, list[str]] = {}
def analyze_manifest(self):
self.manifest_files = mf = {}
if not self.distribution.include_package_data:
return
src_dirs: dict[str, str] = {}
src_dirs = {}
for package in self.packages or ():
# Locate package source directory
src_dirs[assert_relative(self.get_package_dir(package))] = package
if (
self.existing_egg_info_dir
getattr(self, 'existing_egg_info_dir', None)
and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
):
egg_info_dir = self.existing_egg_info_dir
@@ -210,11 +200,9 @@ class build_py(orig.build_py):
importable = check.importable_subpackage(src_dirs[d], f)
if importable:
check.warn(importable)
self.manifest_files.setdefault(src_dirs[d], []).append(path)
mf.setdefault(src_dirs[d], []).append(path)
def _filter_build_files(
self, files: Iterable[str], egg_info: StrPath
) -> Iterator[str]:
def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]:
"""
``build_meta`` may try to create egg_info outside of the project directory,
and this can be problematic for certain plugins (reported in issue #3500).
@@ -233,7 +221,7 @@ class build_py(orig.build_py):
if not os.path.isabs(file) or all(d not in norm_path for d in norm_dirs):
yield file
def get_data_files(self) -> None:
def get_data_files(self):
pass # Lazily compute data files in _get_data_files() function.
def check_package(self, package, package_dir):
@@ -255,14 +243,14 @@ class build_py(orig.build_py):
else:
return init_py
with open(init_py, 'rb') as f:
with io.open(init_py, 'rb') as f:
contents = f.read()
if b'declare_namespace' not in contents:
raise distutils.errors.DistutilsError(
f"Namespace package problem: {package} is a namespace package, but "
"Namespace package problem: %s is a namespace package, but "
"its\n__init__.py does not call declare_namespace()! Please "
'fix it.\n(See the setuptools manual under '
'"Namespace Packages" for details.)\n"'
'"Namespace Packages" for details.)\n"' % (package,)
)
return init_py
@@ -295,7 +283,7 @@ class build_py(orig.build_py):
return list(unique_everseen(keepers))
@staticmethod
def _get_platform_patterns(spec, package, src_dir, extra_patterns=()):
def _get_platform_patterns(spec, package, src_dir):
"""
yield platform-specific path patterns (suitable for glob
or fn_match) from a glob-based spec (such as
@@ -303,7 +291,6 @@ class build_py(orig.build_py):
matching package in src_dir.
"""
raw_patterns = itertools.chain(
extra_patterns,
spec.get('', []),
spec.get(package, []),
)
@@ -338,54 +325,34 @@ def assert_relative(path):
class _IncludePackageDataAbuse:
"""Inform users that package or module is included as 'data file'"""
class _Warning(SetuptoolsDeprecationWarning):
_SUMMARY = """
Package {importable!r} is absent from the `packages` configuration.
"""
MESSAGE = """\
Installing {importable!r} as data is deprecated, please list it in `packages`.
!!\n\n
############################
# Package would be ignored #
############################
Python recognizes {importable!r} as an importable package,
but it is not listed in the `packages` configuration of setuptools.
_DETAILS = """
############################
# Package would be ignored #
############################
Python recognizes {importable!r} as an importable package[^1],
but it is absent from setuptools' `packages` configuration.
{importable!r} has been automatically added to the distribution only
because it may contain data files, but this behavior is likely to change
in future versions of setuptools (and therefore is considered deprecated).
This leads to an ambiguous overall configuration. If you want to distribute this
package, please make sure that {importable!r} is explicitly added
to the `packages` configuration field.
Please make sure that {importable!r} is included as a package by using
the `packages` configuration field or the proper discovery methods
(for example by using `find_namespace_packages(...)`/`find_namespace:`
instead of `find_packages(...)`/`find:`).
Alternatively, you can also rely on setuptools' discovery methods
(for example by using `find_namespace_packages(...)`/`find_namespace:`
instead of `find_packages(...)`/`find:`).
You can read more about "package discovery" on setuptools documentation page:
- https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
If you don't want {importable!r} to be distributed and are
already explicitly excluding {importable!r} via
`find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`,
you can try to use `exclude_package_data`, or `include-package-data=False` in
combination with a more fine grained `package-data` configuration.
You can read more about "package data files" on setuptools documentation page:
- https://setuptools.pypa.io/en/latest/userguide/datafiles.html
[^1]: For Python, any directory (with suitable naming) can be imported,
even if it does not contain any `.py` files.
On the other hand, currently there is no concept of package data
directory, all directories are treated like packages.
"""
# _DUE_DATE: still not defined as this is particularly controversial.
# Warning initially introduced in May 2022. See issue #3340 for discussion.
You can read more about "package discovery" and "data files" on setuptools
documentation page.
\n\n!!
"""
def __init__(self):
self._already_warned = set()
def is_module(self, file):
return file.endswith(".py") and file[: -len(".py")].isidentifier()
return file.endswith(".py") and file[:-len(".py")].isidentifier()
def importable_subpackage(self, parent, file):
pkg = Path(file).parent
@@ -396,5 +363,6 @@ class _IncludePackageDataAbuse:
def warn(self, importable):
if importable not in self._already_warned:
self._Warning.emit(importable=importable)
msg = textwrap.dedent(self.MESSAGE).format(importable=importable)
warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
self._already_warned.add(importable)