|
from __future__ import annotations |
|
|
|
import functools |
|
import io |
|
import itertools |
|
import numbers |
|
import os |
|
import re |
|
import sys |
|
from collections.abc import Iterable, Iterator, MutableMapping, Sequence |
|
from glob import glob |
|
from pathlib import Path |
|
from typing import TYPE_CHECKING, Any, Union |
|
|
|
from more_itertools import partition, unique_everseen |
|
from packaging.markers import InvalidMarker, Marker |
|
from packaging.specifiers import InvalidSpecifier, SpecifierSet |
|
from packaging.version import Version |
|
|
|
from . import ( |
|
_entry_points, |
|
_reqs, |
|
_static, |
|
command as _, |
|
) |
|
from ._importlib import metadata |
|
from ._normalization import _canonicalize_license_expression |
|
from ._path import StrPath |
|
from ._reqs import _StrOrIter |
|
from .config import pyprojecttoml, setupcfg |
|
from .discovery import ConfigDiscovery |
|
from .errors import InvalidConfigError |
|
from .monkey import get_unpatched |
|
from .warnings import InformationOnly, SetuptoolsDeprecationWarning |
|
|
|
import distutils.cmd |
|
import distutils.command |
|
import distutils.core |
|
import distutils.dist |
|
import distutils.log |
|
from distutils.debug import DEBUG |
|
from distutils.errors import DistutilsOptionError, DistutilsSetupError |
|
from distutils.fancy_getopt import translate_longopt |
|
from distutils.util import strtobool |
|
|
|
if TYPE_CHECKING: |
|
from typing_extensions import TypeAlias |
|
|
|
|
|
__all__ = ['Distribution'] |
|
|
|
_sequence = tuple, list |
|
""" |
|
:meta private: |
|
|
|
Supported iterable types that are known to be: |
|
- ordered (which `set` isn't) |
|
- not match a str (which `Sequence[str]` does) |
|
- not imply a nested type (like `dict`) |
|
for use with `isinstance`. |
|
""" |
|
_Sequence: TypeAlias = Union[tuple[str, ...], list[str]] |
|
|
|
_sequence_type_repr = "tuple[str, ...] | list[str]" |
|
_OrderedStrSequence: TypeAlias = Union[str, dict[str, Any], Sequence[str]] |
|
""" |
|
:meta private: |
|
Avoid single-use iterable. Disallow sets. |
|
A poor approximation of an OrderedSequence (dict doesn't match a Sequence). |
|
""" |
|
|
|
|
|
def __getattr__(name: str) -> Any: |
|
if name == "sequence": |
|
SetuptoolsDeprecationWarning.emit( |
|
"`setuptools.dist.sequence` is an internal implementation detail.", |
|
"Please define your own `sequence = tuple, list` instead.", |
|
due_date=(2025, 8, 28), |
|
) |
|
return _sequence |
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") |
|
|
|
|
|
def check_importable(dist, attr, value): |
|
try: |
|
ep = metadata.EntryPoint(value=value, name=None, group=None) |
|
assert not ep.extras |
|
except (TypeError, ValueError, AttributeError, AssertionError) as e: |
|
raise DistutilsSetupError( |
|
f"{attr!r} must be importable 'module:attrs' string (got {value!r})" |
|
) from e |
|
|
|
|
|
def assert_string_list(dist, attr: str, value: _Sequence) -> None: |
|
"""Verify that value is a string list""" |
|
try: |
|
|
|
|
|
assert isinstance(value, _sequence) |
|
|
|
assert ''.join(value) != value |
|
except (TypeError, ValueError, AttributeError, AssertionError) as e: |
|
raise DistutilsSetupError( |
|
f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})" |
|
) from e |
|
|
|
|
|
def check_nsp(dist, attr, value): |
|
"""Verify that namespace packages are valid""" |
|
ns_packages = value |
|
assert_string_list(dist, attr, ns_packages) |
|
for nsp in ns_packages: |
|
if not dist.has_contents_for(nsp): |
|
raise DistutilsSetupError( |
|
f"Distribution contains no modules or packages for namespace package {nsp!r}" |
|
) |
|
parent, _sep, _child = nsp.rpartition('.') |
|
if parent and parent not in ns_packages: |
|
distutils.log.warn( |
|
"WARNING: %r is declared as a package namespace, but %r" |
|
" is not: please correct this in setup.py", |
|
nsp, |
|
parent, |
|
) |
|
SetuptoolsDeprecationWarning.emit( |
|
"The namespace_packages parameter is deprecated.", |
|
"Please replace its usage with implicit namespaces (PEP 420).", |
|
see_docs="references/keywords.html#keyword-namespace-packages", |
|
|
|
|
|
|
|
|
|
) |
|
|
|
|
|
def check_extras(dist, attr, value): |
|
"""Verify that extras_require mapping is valid""" |
|
try: |
|
list(itertools.starmap(_check_extra, value.items())) |
|
except (TypeError, ValueError, AttributeError) as e: |
|
raise DistutilsSetupError( |
|
"'extras_require' must be a dictionary whose values are " |
|
"strings or lists of strings containing valid project/version " |
|
"requirement specifiers." |
|
) from e |
|
|
|
|
|
def _check_extra(extra, reqs): |
|
_name, _sep, marker = extra.partition(':') |
|
try: |
|
_check_marker(marker) |
|
except InvalidMarker: |
|
msg = f"Invalid environment marker: {marker} ({extra!r})" |
|
raise DistutilsSetupError(msg) from None |
|
list(_reqs.parse(reqs)) |
|
|
|
|
|
def _check_marker(marker): |
|
if not marker: |
|
return |
|
m = Marker(marker) |
|
m.evaluate() |
|
|
|
|
|
def assert_bool(dist, attr, value): |
|
"""Verify that value is True, False, 0, or 1""" |
|
if bool(value) != value: |
|
raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})") |
|
|
|
|
|
def invalid_unless_false(dist, attr, value): |
|
if not value: |
|
DistDeprecationWarning.emit(f"{attr} is ignored.") |
|
|
|
return |
|
raise DistutilsSetupError(f"{attr} is invalid.") |
|
|
|
|
|
def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None: |
|
"""Verify that install_requires is a valid requirements list""" |
|
try: |
|
list(_reqs.parse(value)) |
|
if isinstance(value, set): |
|
raise TypeError("Unordered types are not allowed") |
|
except (TypeError, ValueError) as error: |
|
msg = ( |
|
f"{attr!r} must be a string or iterable of strings " |
|
f"containing valid project/version requirement specifiers; {error}" |
|
) |
|
raise DistutilsSetupError(msg) from error |
|
|
|
|
|
def check_specifier(dist, attr, value): |
|
"""Verify that value is a valid version specifier""" |
|
try: |
|
SpecifierSet(value) |
|
except (InvalidSpecifier, AttributeError) as error: |
|
msg = f"{attr!r} must be a string containing valid version specifiers; {error}" |
|
raise DistutilsSetupError(msg) from error |
|
|
|
|
|
def check_entry_points(dist, attr, value): |
|
"""Verify that entry_points map is parseable""" |
|
try: |
|
_entry_points.load(value) |
|
except Exception as e: |
|
raise DistutilsSetupError(e) from e |
|
|
|
|
|
def check_package_data(dist, attr, value): |
|
"""Verify that value is a dictionary of package names to glob lists""" |
|
if not isinstance(value, dict): |
|
raise DistutilsSetupError( |
|
f"{attr!r} must be a dictionary mapping package names to lists of " |
|
"string wildcard patterns" |
|
) |
|
for k, v in value.items(): |
|
if not isinstance(k, str): |
|
raise DistutilsSetupError( |
|
f"keys of {attr!r} dict must be strings (got {k!r})" |
|
) |
|
assert_string_list(dist, f'values of {attr!r} dict', v) |
|
|
|
|
|
def check_packages(dist, attr, value): |
|
for pkgname in value: |
|
if not re.match(r'\w+(\.\w+)*', pkgname): |
|
distutils.log.warn( |
|
"WARNING: %r not a valid package name; please use only " |
|
".-separated package names in setup.py", |
|
pkgname, |
|
) |
|
|
|
|
|
if TYPE_CHECKING: |
|
|
|
from distutils.core import Distribution as _Distribution |
|
else: |
|
_Distribution = get_unpatched(distutils.core.Distribution) |
|
|
|
|
|
class Distribution(_Distribution): |
|
"""Distribution with support for tests and package data |
|
|
|
This is an enhanced version of 'distutils.dist.Distribution' that |
|
effectively adds the following new optional keyword arguments to 'setup()': |
|
|
|
'install_requires' -- a string or sequence of strings specifying project |
|
versions that the distribution requires when installed, in the format |
|
used by 'pkg_resources.require()'. They will be installed |
|
automatically when the package is installed. If you wish to use |
|
packages that are not available in PyPI, or want to give your users an |
|
alternate download location, you can add a 'find_links' option to the |
|
'[easy_install]' section of your project's 'setup.cfg' file, and then |
|
setuptools will scan the listed web pages for links that satisfy the |
|
requirements. |
|
|
|
'extras_require' -- a dictionary mapping names of optional "extras" to the |
|
additional requirement(s) that using those extras incurs. For example, |
|
this:: |
|
|
|
extras_require = dict(reST = ["docutils>=0.3", "reSTedit"]) |
|
|
|
indicates that the distribution can optionally provide an extra |
|
capability called "reST", but it can only be used if docutils and |
|
reSTedit are installed. If the user installs your package using |
|
EasyInstall and requests one of your extras, the corresponding |
|
additional requirements will be installed if needed. |
|
|
|
'package_data' -- a dictionary mapping package names to lists of filenames |
|
or globs to use to find data files contained in the named packages. |
|
If the dictionary has filenames or globs listed under '""' (the empty |
|
string), those names will be searched for in every package, in addition |
|
to any names for the specific package. Data files found using these |
|
names/globs will be installed along with the package, in the same |
|
location as the package. Note that globs are allowed to reference |
|
the contents of non-package subdirectories, as long as you use '/' as |
|
a path separator. (Globs are automatically converted to |
|
platform-specific paths at runtime.) |
|
|
|
In addition to these new keywords, this class also has several new methods |
|
for manipulating the distribution's contents. For example, the 'include()' |
|
and 'exclude()' methods can be thought of as in-place add and subtract |
|
commands that add or remove packages, modules, extensions, and so on from |
|
the distribution. |
|
""" |
|
|
|
_DISTUTILS_UNSUPPORTED_METADATA = { |
|
'long_description_content_type': lambda: None, |
|
'project_urls': dict, |
|
'provides_extras': dict, |
|
'license_expression': lambda: None, |
|
'license_file': lambda: None, |
|
'license_files': lambda: None, |
|
'install_requires': list, |
|
'extras_require': dict, |
|
} |
|
|
|
|
|
namespace_packages: list[str] |
|
|
|
|
|
def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: |
|
have_package_data = hasattr(self, "package_data") |
|
if not have_package_data: |
|
self.package_data: dict[str, list[str]] = {} |
|
attrs = attrs or {} |
|
self.dist_files: list[tuple[str, str, str]] = [] |
|
self.include_package_data: bool | None = None |
|
self.exclude_package_data: dict[str, list[str]] | None = None |
|
|
|
self.src_root: str | None = attrs.pop("src_root", None) |
|
self.dependency_links: list[str] = attrs.pop('dependency_links', []) |
|
self.setup_requires: list[str] = attrs.pop('setup_requires', []) |
|
for ep in metadata.entry_points(group='distutils.setup_keywords'): |
|
vars(self).setdefault(ep.name, None) |
|
|
|
metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA) |
|
metadata_only -= {"install_requires", "extras_require"} |
|
dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} |
|
_Distribution.__init__(self, dist_attrs) |
|
|
|
|
|
|
|
|
|
self._referenced_files = set[str]() |
|
|
|
self.set_defaults = ConfigDiscovery(self) |
|
|
|
self._set_metadata_defaults(attrs) |
|
|
|
self.metadata.version = self._normalize_version(self.metadata.version) |
|
self._finalize_requires() |
|
|
|
def _validate_metadata(self): |
|
required = {"name"} |
|
provided = { |
|
key |
|
for key in vars(self.metadata) |
|
if getattr(self.metadata, key, None) is not None |
|
} |
|
missing = required - provided |
|
|
|
if missing: |
|
msg = f"Required package metadata is missing: {missing}" |
|
raise DistutilsSetupError(msg) |
|
|
|
def _set_metadata_defaults(self, attrs): |
|
""" |
|
Fill-in missing metadata fields not supported by distutils. |
|
Some fields may have been set by other tools (e.g. pbr). |
|
Those fields (vars(self.metadata)) take precedence to |
|
supplied attrs. |
|
""" |
|
for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items(): |
|
vars(self.metadata).setdefault(option, attrs.get(option, default())) |
|
|
|
@staticmethod |
|
def _normalize_version(version): |
|
from . import sic |
|
|
|
if isinstance(version, numbers.Number): |
|
|
|
version = str(version) |
|
elif isinstance(version, sic) or version is None: |
|
return version |
|
|
|
normalized = str(Version(version)) |
|
if version != normalized: |
|
InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'") |
|
return normalized |
|
return version |
|
|
|
def _finalize_requires(self): |
|
""" |
|
Set `metadata.python_requires` and fix environment markers |
|
in `install_requires` and `extras_require`. |
|
""" |
|
if getattr(self, 'python_requires', None): |
|
self.metadata.python_requires = self.python_requires |
|
|
|
self._normalize_requires() |
|
self.metadata.install_requires = self.install_requires |
|
self.metadata.extras_require = self.extras_require |
|
|
|
if self.extras_require: |
|
for extra in self.extras_require.keys(): |
|
|
|
extra = extra.split(':')[0] |
|
if extra: |
|
self.metadata.provides_extras.setdefault(extra) |
|
|
|
def _normalize_requires(self): |
|
"""Make sure requirement-related attributes exist and are normalized""" |
|
install_requires = getattr(self, "install_requires", None) or [] |
|
extras_require = getattr(self, "extras_require", None) or {} |
|
|
|
|
|
list_ = _static.List if _static.is_static(install_requires) else list |
|
self.install_requires = list_(map(str, _reqs.parse(install_requires))) |
|
|
|
dict_ = _static.Dict if _static.is_static(extras_require) else dict |
|
self.extras_require = dict_( |
|
(k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items() |
|
) |
|
|
|
def _finalize_license_expression(self) -> None: |
|
""" |
|
Normalize license and license_expression. |
|
>>> dist = Distribution({"license_expression": _static.Str("mit aNd gpl-3.0-OR-later")}) |
|
>>> _static.is_static(dist.metadata.license_expression) |
|
True |
|
>>> dist._finalize_license_expression() |
|
>>> _static.is_static(dist.metadata.license_expression) # preserve "static-ness" |
|
True |
|
>>> print(dist.metadata.license_expression) |
|
MIT AND GPL-3.0-or-later |
|
""" |
|
classifiers = self.metadata.get_classifiers() |
|
license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")] |
|
|
|
license_expr = self.metadata.license_expression |
|
if license_expr: |
|
str_ = _static.Str if _static.is_static(license_expr) else str |
|
normalized = str_(_canonicalize_license_expression(license_expr)) |
|
if license_expr != normalized: |
|
InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") |
|
self.metadata.license_expression = normalized |
|
if license_classifiers: |
|
raise InvalidConfigError( |
|
"License classifiers have been superseded by license expressions " |
|
"(see https://peps.python.org/pep-0639/). Please remove:\n\n" |
|
+ "\n".join(license_classifiers), |
|
) |
|
elif license_classifiers: |
|
pypa_guides = "guides/writing-pyproject-toml/#license" |
|
SetuptoolsDeprecationWarning.emit( |
|
"License classifiers are deprecated.", |
|
"Please consider removing the following classifiers in favor of a " |
|
"SPDX license expression:\n\n" + "\n".join(license_classifiers), |
|
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", |
|
|
|
|
|
|
|
) |
|
|
|
def _finalize_license_files(self) -> None: |
|
"""Compute names of all license files which should be included.""" |
|
license_files: list[str] | None = self.metadata.license_files |
|
patterns = license_files or [] |
|
|
|
license_file: str | None = self.metadata.license_file |
|
if license_file and license_file not in patterns: |
|
patterns.append(license_file) |
|
|
|
if license_files is None and license_file is None: |
|
|
|
|
|
|
|
patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] |
|
files = self._expand_patterns(patterns, enforce_match=False) |
|
else: |
|
files = self._expand_patterns(patterns, enforce_match=True) |
|
|
|
self.metadata.license_files = list(unique_everseen(files)) |
|
|
|
@classmethod |
|
def _expand_patterns( |
|
cls, patterns: list[str], enforce_match: bool = True |
|
) -> Iterator[str]: |
|
""" |
|
>>> getfixture('sample_project_cwd') |
|
>>> list(Distribution._expand_patterns(['LICENSE.txt'])) |
|
['LICENSE.txt'] |
|
>>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*'])) |
|
['pyproject.toml', 'LICENSE.txt'] |
|
>>> list(Distribution._expand_patterns(['src/**/*.dat'])) |
|
['src/sample/package_data.dat'] |
|
""" |
|
return ( |
|
path.replace(os.sep, "/") |
|
for pattern in patterns |
|
for path in sorted(cls._find_pattern(pattern, enforce_match)) |
|
if not path.endswith('~') and os.path.isfile(path) |
|
) |
|
|
|
@staticmethod |
|
def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]: |
|
r""" |
|
>>> getfixture('sample_project_cwd') |
|
>>> Distribution._find_pattern("LICENSE.txt") |
|
['LICENSE.txt'] |
|
>>> Distribution._find_pattern("/LICENSE.MIT") |
|
Traceback (most recent call last): |
|
... |
|
setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative... |
|
>>> Distribution._find_pattern("../LICENSE.MIT") |
|
Traceback (most recent call last): |
|
... |
|
setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern '../LICENSE.MIT' cannot contain '..'... |
|
>>> Distribution._find_pattern("LICEN{CSE*") |
|
Traceback (most recent call last): |
|
... |
|
setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters... |
|
""" |
|
pypa_guides = "specifications/glob-patterns/" |
|
if ".." in pattern: |
|
SetuptoolsDeprecationWarning.emit( |
|
f"Pattern {pattern!r} cannot contain '..'", |
|
""" |
|
Please ensure the files specified are contained by the root |
|
of the Python package (normally marked by `pyproject.toml`). |
|
""", |
|
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", |
|
due_date=(2026, 3, 20), |
|
|
|
) |
|
if pattern.startswith((os.sep, "/")) or ":\\" in pattern: |
|
raise InvalidConfigError( |
|
f"Pattern {pattern!r} should be relative and must not start with '/'" |
|
) |
|
if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None: |
|
SetuptoolsDeprecationWarning.emit( |
|
"Please provide a valid glob pattern.", |
|
"Pattern {pattern!r} contains invalid characters.", |
|
pattern=pattern, |
|
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", |
|
due_date=(2026, 3, 20), |
|
) |
|
|
|
found = glob(pattern, recursive=True) |
|
|
|
if enforce_match and not found: |
|
SetuptoolsDeprecationWarning.emit( |
|
"Cannot find any files for the given pattern.", |
|
"Pattern {pattern!r} did not match any files.", |
|
pattern=pattern, |
|
due_date=(2026, 3, 20), |
|
|
|
|
|
|
|
) |
|
return found |
|
|
|
|
|
def _parse_config_files(self, filenames=None): |
|
""" |
|
Adapted from distutils.dist.Distribution.parse_config_files, |
|
this method provides the same functionality in subtly-improved |
|
ways. |
|
""" |
|
from configparser import ConfigParser |
|
|
|
|
|
ignore_options = ( |
|
[] |
|
if sys.prefix == sys.base_prefix |
|
else [ |
|
'install-base', |
|
'install-platbase', |
|
'install-lib', |
|
'install-platlib', |
|
'install-purelib', |
|
'install-headers', |
|
'install-scripts', |
|
'install-data', |
|
'prefix', |
|
'exec-prefix', |
|
'home', |
|
'user', |
|
'root', |
|
] |
|
) |
|
|
|
ignore_options = frozenset(ignore_options) |
|
|
|
if filenames is None: |
|
filenames = self.find_config_files() |
|
|
|
if DEBUG: |
|
self.announce("Distribution.parse_config_files():") |
|
|
|
parser = ConfigParser() |
|
parser.optionxform = str |
|
for filename in filenames: |
|
with open(filename, encoding='utf-8') as reader: |
|
if DEBUG: |
|
self.announce(" reading {filename}".format(**locals())) |
|
parser.read_file(reader) |
|
for section in parser.sections(): |
|
options = parser.options(section) |
|
opt_dict = self.get_option_dict(section) |
|
|
|
for opt in options: |
|
if opt == '__name__' or opt in ignore_options: |
|
continue |
|
|
|
val = parser.get(section, opt) |
|
opt = self._enforce_underscore(opt, section) |
|
opt = self._enforce_option_lowercase(opt, section) |
|
opt_dict[opt] = (filename, val) |
|
|
|
|
|
|
|
parser.__init__() |
|
|
|
if 'global' not in self.command_options: |
|
return |
|
|
|
|
|
|
|
|
|
for opt, (src, val) in self.command_options['global'].items(): |
|
alias = self.negative_opt.get(opt) |
|
if alias: |
|
val = not strtobool(val) |
|
elif opt in ('verbose', 'dry_run'): |
|
val = strtobool(val) |
|
|
|
try: |
|
setattr(self, alias or opt, val) |
|
except ValueError as e: |
|
raise DistutilsOptionError(e) from e |
|
|
|
def _enforce_underscore(self, opt: str, section: str) -> str: |
|
if "-" not in opt or self._skip_setupcfg_normalization(section): |
|
return opt |
|
|
|
underscore_opt = opt.replace('-', '_') |
|
affected = f"(Affected: {self.metadata.name})." if self.metadata.name else "" |
|
SetuptoolsDeprecationWarning.emit( |
|
f"Invalid dash-separated key {opt!r} in {section!r} (setup.cfg), " |
|
f"please use the underscore name {underscore_opt!r} instead.", |
|
f""" |
|
Usage of dash-separated {opt!r} will not be supported in future |
|
versions. Please use the underscore name {underscore_opt!r} instead. |
|
{affected} |
|
""", |
|
see_docs="userguide/declarative_config.html", |
|
due_date=(2026, 3, 3), |
|
|
|
) |
|
return underscore_opt |
|
|
|
def _enforce_option_lowercase(self, opt: str, section: str) -> str: |
|
if opt.islower() or self._skip_setupcfg_normalization(section): |
|
return opt |
|
|
|
lowercase_opt = opt.lower() |
|
affected = f"(Affected: {self.metadata.name})." if self.metadata.name else "" |
|
SetuptoolsDeprecationWarning.emit( |
|
f"Invalid uppercase key {opt!r} in {section!r} (setup.cfg), " |
|
f"please use lowercase {lowercase_opt!r} instead.", |
|
f""" |
|
Usage of uppercase key {opt!r} in {section!r} will not be supported in |
|
future versions. Please use lowercase {lowercase_opt!r} instead. |
|
{affected} |
|
""", |
|
see_docs="userguide/declarative_config.html", |
|
due_date=(2026, 3, 3), |
|
|
|
) |
|
return lowercase_opt |
|
|
|
def _skip_setupcfg_normalization(self, section: str) -> bool: |
|
skip = ( |
|
'options.extras_require', |
|
'options.data_files', |
|
'options.entry_points', |
|
'options.package_data', |
|
'options.exclude_package_data', |
|
) |
|
return section in skip or not self._is_setuptools_section(section) |
|
|
|
def _is_setuptools_section(self, section: str) -> bool: |
|
return ( |
|
section == "metadata" |
|
or section.startswith("options") |
|
or section in _setuptools_commands() |
|
) |
|
|
|
|
|
def _set_command_options(self, command_obj, option_dict=None): |
|
""" |
|
Set the options for 'command_obj' from 'option_dict'. Basically |
|
this means copying elements of a dictionary ('option_dict') to |
|
attributes of an instance ('command'). |
|
|
|
'command_obj' must be a Command instance. If 'option_dict' is not |
|
supplied, uses the standard option dictionary for this command |
|
(from 'self.command_options'). |
|
|
|
(Adopted from distutils.dist.Distribution._set_command_options) |
|
""" |
|
command_name = command_obj.get_command_name() |
|
if option_dict is None: |
|
option_dict = self.get_option_dict(command_name) |
|
|
|
if DEBUG: |
|
self.announce(f" setting options for '{command_name}' command:") |
|
for option, (source, value) in option_dict.items(): |
|
if DEBUG: |
|
self.announce(f" {option} = {value} (from {source})") |
|
try: |
|
bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] |
|
except AttributeError: |
|
bool_opts = [] |
|
try: |
|
neg_opt = command_obj.negative_opt |
|
except AttributeError: |
|
neg_opt = {} |
|
|
|
try: |
|
is_string = isinstance(value, str) |
|
if option in neg_opt and is_string: |
|
setattr(command_obj, neg_opt[option], not strtobool(value)) |
|
elif option in bool_opts and is_string: |
|
setattr(command_obj, option, strtobool(value)) |
|
elif hasattr(command_obj, option): |
|
setattr(command_obj, option, value) |
|
else: |
|
raise DistutilsOptionError( |
|
f"error in {source}: command '{command_name}' has no such option '{option}'" |
|
) |
|
except ValueError as e: |
|
raise DistutilsOptionError(e) from e |
|
|
|
def _get_project_config_files(self, filenames: Iterable[StrPath] | None): |
|
"""Add default file and split between INI and TOML""" |
|
tomlfiles = [] |
|
standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml") |
|
if filenames is not None: |
|
parts = partition(lambda f: Path(f).suffix == ".toml", filenames) |
|
filenames = list(parts[0]) |
|
tomlfiles = list(parts[1]) |
|
elif standard_project_metadata.exists(): |
|
tomlfiles = [standard_project_metadata] |
|
return filenames, tomlfiles |
|
|
|
def parse_config_files( |
|
self, |
|
filenames: Iterable[StrPath] | None = None, |
|
ignore_option_errors: bool = False, |
|
) -> None: |
|
"""Parses configuration files from various levels |
|
and loads configuration. |
|
""" |
|
inifiles, tomlfiles = self._get_project_config_files(filenames) |
|
|
|
self._parse_config_files(filenames=inifiles) |
|
|
|
setupcfg.parse_configuration( |
|
self, self.command_options, ignore_option_errors=ignore_option_errors |
|
) |
|
for filename in tomlfiles: |
|
pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) |
|
|
|
self._finalize_requires() |
|
self._finalize_license_expression() |
|
self._finalize_license_files() |
|
|
|
def fetch_build_eggs(self, requires: _StrOrIter) -> list[metadata.Distribution]: |
|
"""Resolve pre-setup requirements""" |
|
from .installer import _fetch_build_eggs |
|
|
|
return _fetch_build_eggs(self, requires) |
|
|
|
def finalize_options(self) -> None: |
|
""" |
|
Allow plugins to apply arbitrary operations to the |
|
distribution. Each hook may optionally define a 'order' |
|
to influence the order of execution. Smaller numbers |
|
go first and the default is 0. |
|
""" |
|
group = 'setuptools.finalize_distribution_options' |
|
|
|
def by_order(hook): |
|
return getattr(hook, 'order', 0) |
|
|
|
defined = metadata.entry_points(group=group) |
|
filtered = itertools.filterfalse(self._removed, defined) |
|
loaded = map(lambda e: e.load(), filtered) |
|
for ep in sorted(loaded, key=by_order): |
|
ep(self) |
|
|
|
@staticmethod |
|
def _removed(ep): |
|
""" |
|
When removing an entry point, if metadata is loaded |
|
from an older version of Setuptools, that removed |
|
entry point will attempt to be loaded and will fail. |
|
See #2765 for more details. |
|
""" |
|
removed = { |
|
|
|
'2to3_doctests', |
|
} |
|
return ep.name in removed |
|
|
|
def _finalize_setup_keywords(self): |
|
for ep in metadata.entry_points(group='distutils.setup_keywords'): |
|
value = getattr(self, ep.name, None) |
|
if value is not None: |
|
ep.load()(self, ep.name, value) |
|
|
|
def get_egg_cache_dir(self): |
|
from . import windows_support |
|
|
|
egg_cache_dir = os.path.join(os.curdir, '.eggs') |
|
if not os.path.exists(egg_cache_dir): |
|
os.mkdir(egg_cache_dir) |
|
windows_support.hide_file(egg_cache_dir) |
|
readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt') |
|
with open(readme_txt_filename, 'w', encoding="utf-8") as f: |
|
f.write( |
|
'This directory contains eggs that were downloaded ' |
|
'by setuptools to build, test, and run plug-ins.\n\n' |
|
) |
|
f.write( |
|
'This directory caches those eggs to prevent ' |
|
'repeated downloads.\n\n' |
|
) |
|
f.write('However, it is safe to delete this directory.\n\n') |
|
|
|
return egg_cache_dir |
|
|
|
def fetch_build_egg(self, req): |
|
"""Fetch an egg needed for building""" |
|
from .installer import fetch_build_egg |
|
|
|
return fetch_build_egg(self, req) |
|
|
|
def get_command_class(self, command: str) -> type[distutils.cmd.Command]: |
|
"""Pluggable version of get_command_class()""" |
|
if command in self.cmdclass: |
|
return self.cmdclass[command] |
|
|
|
|
|
if command == 'bdist_wheel': |
|
from .command.bdist_wheel import bdist_wheel |
|
|
|
return bdist_wheel |
|
|
|
eps = metadata.entry_points(group='distutils.commands', name=command) |
|
for ep in eps: |
|
self.cmdclass[command] = cmdclass = ep.load() |
|
return cmdclass |
|
else: |
|
return _Distribution.get_command_class(self, command) |
|
|
|
def print_commands(self): |
|
for ep in metadata.entry_points(group='distutils.commands'): |
|
if ep.name not in self.cmdclass: |
|
cmdclass = ep.load() |
|
self.cmdclass[ep.name] = cmdclass |
|
return _Distribution.print_commands(self) |
|
|
|
def get_command_list(self): |
|
for ep in metadata.entry_points(group='distutils.commands'): |
|
if ep.name not in self.cmdclass: |
|
cmdclass = ep.load() |
|
self.cmdclass[ep.name] = cmdclass |
|
return _Distribution.get_command_list(self) |
|
|
|
def include(self, **attrs) -> None: |
|
"""Add items to distribution that are named in keyword arguments |
|
|
|
For example, 'dist.include(py_modules=["x"])' would add 'x' to |
|
the distribution's 'py_modules' attribute, if it was not already |
|
there. |
|
|
|
Currently, this method only supports inclusion for attributes that are |
|
lists or tuples. If you need to add support for adding to other |
|
attributes in this or a subclass, you can add an '_include_X' method, |
|
where 'X' is the name of the attribute. The method will be called with |
|
the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})' |
|
will try to call 'dist._include_foo({"bar":"baz"})', which can then |
|
handle whatever special inclusion logic is needed. |
|
""" |
|
for k, v in attrs.items(): |
|
include = getattr(self, '_include_' + k, None) |
|
if include: |
|
include(v) |
|
else: |
|
self._include_misc(k, v) |
|
|
|
def exclude_package(self, package: str) -> None: |
|
"""Remove packages, modules, and extensions in named package""" |
|
|
|
pfx = package + '.' |
|
if self.packages: |
|
self.packages = [ |
|
p for p in self.packages if p != package and not p.startswith(pfx) |
|
] |
|
|
|
if self.py_modules: |
|
self.py_modules = [ |
|
p for p in self.py_modules if p != package and not p.startswith(pfx) |
|
] |
|
|
|
if self.ext_modules: |
|
self.ext_modules = [ |
|
p |
|
for p in self.ext_modules |
|
if p.name != package and not p.name.startswith(pfx) |
|
] |
|
|
|
def has_contents_for(self, package: str) -> bool: |
|
"""Return true if 'exclude_package(package)' would do something""" |
|
|
|
pfx = package + '.' |
|
|
|
for p in self.iter_distribution_names(): |
|
if p == package or p.startswith(pfx): |
|
return True |
|
|
|
return False |
|
|
|
def _exclude_misc(self, name: str, value: _Sequence) -> None: |
|
"""Handle 'exclude()' for list/tuple attrs without a special handler""" |
|
if not isinstance(value, _sequence): |
|
raise DistutilsSetupError( |
|
f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" |
|
) |
|
try: |
|
old = getattr(self, name) |
|
except AttributeError as e: |
|
raise DistutilsSetupError(f"{name}: No such distribution setting") from e |
|
if old is not None and not isinstance(old, _sequence): |
|
raise DistutilsSetupError( |
|
name + ": this setting cannot be changed via include/exclude" |
|
) |
|
elif old: |
|
setattr(self, name, [item for item in old if item not in value]) |
|
|
|
def _include_misc(self, name: str, value: _Sequence) -> None: |
|
"""Handle 'include()' for list/tuple attrs without a special handler""" |
|
|
|
if not isinstance(value, _sequence): |
|
raise DistutilsSetupError( |
|
f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" |
|
) |
|
try: |
|
old = getattr(self, name) |
|
except AttributeError as e: |
|
raise DistutilsSetupError(f"{name}: No such distribution setting") from e |
|
if old is None: |
|
setattr(self, name, value) |
|
elif not isinstance(old, _sequence): |
|
raise DistutilsSetupError( |
|
name + ": this setting cannot be changed via include/exclude" |
|
) |
|
else: |
|
new = [item for item in value if item not in old] |
|
setattr(self, name, list(old) + new) |
|
|
|
def exclude(self, **attrs) -> None: |
|
"""Remove items from distribution that are named in keyword arguments |
|
|
|
For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from |
|
the distribution's 'py_modules' attribute. Excluding packages uses |
|
the 'exclude_package()' method, so all of the package's contained |
|
packages, modules, and extensions are also excluded. |
|
|
|
Currently, this method only supports exclusion from attributes that are |
|
lists or tuples. If you need to add support for excluding from other |
|
attributes in this or a subclass, you can add an '_exclude_X' method, |
|
where 'X' is the name of the attribute. The method will be called with |
|
the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})' |
|
will try to call 'dist._exclude_foo({"bar":"baz"})', which can then |
|
handle whatever special exclusion logic is needed. |
|
""" |
|
for k, v in attrs.items(): |
|
exclude = getattr(self, '_exclude_' + k, None) |
|
if exclude: |
|
exclude(v) |
|
else: |
|
self._exclude_misc(k, v) |
|
|
|
def _exclude_packages(self, packages: _Sequence) -> None: |
|
if not isinstance(packages, _sequence): |
|
raise DistutilsSetupError( |
|
f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})" |
|
) |
|
list(map(self.exclude_package, packages)) |
|
|
|
def _parse_command_opts(self, parser, args): |
|
|
|
self.global_options = self.__class__.global_options |
|
self.negative_opt = self.__class__.negative_opt |
|
|
|
|
|
command = args[0] |
|
aliases = self.get_option_dict('aliases') |
|
while command in aliases: |
|
_src, alias = aliases[command] |
|
del aliases[command] |
|
import shlex |
|
|
|
args[:1] = shlex.split(alias, True) |
|
command = args[0] |
|
|
|
nargs = _Distribution._parse_command_opts(self, parser, args) |
|
|
|
|
|
cmd_class = self.get_command_class(command) |
|
if getattr(cmd_class, 'command_consumes_arguments', None): |
|
self.get_option_dict(command)['args'] = ("command line", nargs) |
|
if nargs is not None: |
|
return [] |
|
|
|
return nargs |
|
|
|
def get_cmdline_options(self) -> dict[str, dict[str, str | None]]: |
|
"""Return a '{cmd: {opt:val}}' map of all command-line options |
|
|
|
Option names are all long, but do not include the leading '--', and |
|
contain dashes rather than underscores. If the option doesn't take |
|
an argument (e.g. '--quiet'), the 'val' is 'None'. |
|
|
|
Note that options provided by config files are intentionally excluded. |
|
""" |
|
|
|
d: dict[str, dict[str, str | None]] = {} |
|
|
|
for cmd, opts in self.command_options.items(): |
|
val: str | None |
|
for opt, (src, val) in opts.items(): |
|
if src != "command line": |
|
continue |
|
|
|
opt = opt.replace('_', '-') |
|
|
|
if val == 0: |
|
cmdobj = self.get_command_obj(cmd) |
|
neg_opt = self.negative_opt.copy() |
|
neg_opt.update(getattr(cmdobj, 'negative_opt', {})) |
|
for neg, pos in neg_opt.items(): |
|
if pos == opt: |
|
opt = neg |
|
val = None |
|
break |
|
else: |
|
raise AssertionError("Shouldn't be able to get here") |
|
|
|
elif val == 1: |
|
val = None |
|
|
|
d.setdefault(cmd, {})[opt] = val |
|
|
|
return d |
|
|
|
def iter_distribution_names(self): |
|
"""Yield all packages, modules, and extension names in distribution""" |
|
|
|
yield from self.packages or () |
|
|
|
yield from self.py_modules or () |
|
|
|
for ext in self.ext_modules or (): |
|
if isinstance(ext, tuple): |
|
name, _buildinfo = ext |
|
else: |
|
name = ext.name |
|
if name.endswith('module'): |
|
name = name[:-6] |
|
yield name |
|
|
|
def handle_display_options(self, option_order): |
|
"""If there were any non-global "display-only" options |
|
(--help-commands or the metadata display options) on the command |
|
line, display the requested info and return true; else return |
|
false. |
|
""" |
|
import sys |
|
|
|
if self.help_commands: |
|
return _Distribution.handle_display_options(self, option_order) |
|
|
|
|
|
if not isinstance(sys.stdout, io.TextIOWrapper): |
|
return _Distribution.handle_display_options(self, option_order) |
|
|
|
|
|
|
|
if sys.stdout.encoding.lower() in ('utf-8', 'utf8'): |
|
return _Distribution.handle_display_options(self, option_order) |
|
|
|
|
|
encoding = sys.stdout.encoding |
|
sys.stdout.reconfigure(encoding='utf-8') |
|
try: |
|
return _Distribution.handle_display_options(self, option_order) |
|
finally: |
|
sys.stdout.reconfigure(encoding=encoding) |
|
|
|
def run_command(self, command) -> None: |
|
self.set_defaults() |
|
|
|
|
|
|
|
super().run_command(command) |
|
|
|
|
|
@functools.cache |
|
def _setuptools_commands() -> set[str]: |
|
try: |
|
|
|
entry_points = metadata.distribution('setuptools').entry_points |
|
eps: Iterable[str] = (ep.name for ep in entry_points) |
|
except metadata.PackageNotFoundError: |
|
|
|
eps = [] |
|
return {*distutils.command.__all__, *eps} |
|
|
|
|
|
class DistDeprecationWarning(SetuptoolsDeprecationWarning): |
|
"""Class for warning about deprecations in dist in |
|
setuptools. Not ignored by default, unlike DeprecationWarning.""" |
|
|