jpayne@68: from __future__ import annotations jpayne@68: jpayne@68: import io jpayne@68: import itertools jpayne@68: import numbers jpayne@68: import os jpayne@68: import re jpayne@68: import sys jpayne@68: from collections.abc import Iterable jpayne@68: from glob import iglob jpayne@68: from pathlib import Path jpayne@68: from typing import ( jpayne@68: TYPE_CHECKING, jpayne@68: Any, jpayne@68: Dict, jpayne@68: List, jpayne@68: MutableMapping, jpayne@68: Sequence, jpayne@68: Tuple, jpayne@68: Union, jpayne@68: ) jpayne@68: jpayne@68: from more_itertools import partition, unique_everseen jpayne@68: from packaging.markers import InvalidMarker, Marker jpayne@68: from packaging.specifiers import InvalidSpecifier, SpecifierSet jpayne@68: from packaging.version import Version jpayne@68: jpayne@68: from setuptools._path import StrPath jpayne@68: jpayne@68: from . import ( jpayne@68: _entry_points, jpayne@68: _reqs, jpayne@68: command as _, # noqa: F401 # imported for side-effects jpayne@68: ) jpayne@68: from ._importlib import metadata jpayne@68: from ._reqs import _StrOrIter jpayne@68: from .config import pyprojecttoml, setupcfg jpayne@68: from .discovery import ConfigDiscovery jpayne@68: from .monkey import get_unpatched jpayne@68: from .warnings import InformationOnly, SetuptoolsDeprecationWarning jpayne@68: jpayne@68: import distutils.cmd jpayne@68: import distutils.command jpayne@68: import distutils.core jpayne@68: import distutils.dist jpayne@68: import distutils.log jpayne@68: from distutils.debug import DEBUG jpayne@68: from distutils.errors import DistutilsOptionError, DistutilsSetupError jpayne@68: from distutils.fancy_getopt import translate_longopt jpayne@68: from distutils.util import strtobool jpayne@68: jpayne@68: if TYPE_CHECKING: jpayne@68: from typing_extensions import TypeAlias jpayne@68: jpayne@68: __all__ = ['Distribution'] jpayne@68: jpayne@68: _sequence = tuple, list jpayne@68: """ jpayne@68: :meta private: jpayne@68: jpayne@68: Supported iterable types that are known to be: jpayne@68: - ordered (which `set` isn't) jpayne@68: - not match a str (which `Sequence[str]` does) jpayne@68: - not imply a nested type (like `dict`) jpayne@68: for use with `isinstance`. jpayne@68: """ jpayne@68: _Sequence: TypeAlias = Union[Tuple[str, ...], List[str]] jpayne@68: # This is how stringifying _Sequence would look in Python 3.10 jpayne@68: _sequence_type_repr = "tuple[str, ...] | list[str]" jpayne@68: _OrderedStrSequence: TypeAlias = Union[str, Dict[str, Any], Sequence[str]] jpayne@68: """ jpayne@68: :meta private: jpayne@68: Avoid single-use iterable. Disallow sets. jpayne@68: A poor approximation of an OrderedSequence (dict doesn't match a Sequence). jpayne@68: """ jpayne@68: jpayne@68: jpayne@68: def __getattr__(name: str) -> Any: # pragma: no cover jpayne@68: if name == "sequence": jpayne@68: SetuptoolsDeprecationWarning.emit( jpayne@68: "`setuptools.dist.sequence` is an internal implementation detail.", jpayne@68: "Please define your own `sequence = tuple, list` instead.", jpayne@68: due_date=(2025, 8, 28), # Originally added on 2024-08-27 jpayne@68: ) jpayne@68: return _sequence jpayne@68: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") jpayne@68: jpayne@68: jpayne@68: def check_importable(dist, attr, value): jpayne@68: try: jpayne@68: ep = metadata.EntryPoint(value=value, name=None, group=None) jpayne@68: assert not ep.extras jpayne@68: except (TypeError, ValueError, AttributeError, AssertionError) as e: jpayne@68: raise DistutilsSetupError( jpayne@68: "%r must be importable 'module:attrs' string (got %r)" % (attr, value) jpayne@68: ) from e jpayne@68: jpayne@68: jpayne@68: def assert_string_list(dist, attr: str, value: _Sequence) -> None: jpayne@68: """Verify that value is a string list""" jpayne@68: try: jpayne@68: # verify that value is a list or tuple to exclude unordered jpayne@68: # or single-use iterables jpayne@68: assert isinstance(value, _sequence) jpayne@68: # verify that elements of value are strings jpayne@68: assert ''.join(value) != value jpayne@68: except (TypeError, ValueError, AttributeError, AssertionError) as e: jpayne@68: raise DistutilsSetupError( jpayne@68: f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})" jpayne@68: ) from e jpayne@68: jpayne@68: jpayne@68: def check_nsp(dist, attr, value): jpayne@68: """Verify that namespace packages are valid""" jpayne@68: ns_packages = value jpayne@68: assert_string_list(dist, attr, ns_packages) jpayne@68: for nsp in ns_packages: jpayne@68: if not dist.has_contents_for(nsp): jpayne@68: raise DistutilsSetupError( jpayne@68: "Distribution contains no modules or packages for " jpayne@68: + "namespace package %r" % nsp jpayne@68: ) jpayne@68: parent, sep, child = nsp.rpartition('.') jpayne@68: if parent and parent not in ns_packages: jpayne@68: distutils.log.warn( jpayne@68: "WARNING: %r is declared as a package namespace, but %r" jpayne@68: " is not: please correct this in setup.py", jpayne@68: nsp, jpayne@68: parent, jpayne@68: ) jpayne@68: SetuptoolsDeprecationWarning.emit( jpayne@68: "The namespace_packages parameter is deprecated.", jpayne@68: "Please replace its usage with implicit namespaces (PEP 420).", jpayne@68: see_docs="references/keywords.html#keyword-namespace-packages", jpayne@68: # TODO: define due_date, it may break old packages that are no longer jpayne@68: # maintained (e.g. sphinxcontrib extensions) when installed from source. jpayne@68: # Warning officially introduced in May 2022, however the deprecation jpayne@68: # was mentioned much earlier in the docs (May 2020, see #2149). jpayne@68: ) jpayne@68: jpayne@68: jpayne@68: def check_extras(dist, attr, value): jpayne@68: """Verify that extras_require mapping is valid""" jpayne@68: try: jpayne@68: list(itertools.starmap(_check_extra, value.items())) jpayne@68: except (TypeError, ValueError, AttributeError) as e: jpayne@68: raise DistutilsSetupError( jpayne@68: "'extras_require' must be a dictionary whose values are " jpayne@68: "strings or lists of strings containing valid project/version " jpayne@68: "requirement specifiers." jpayne@68: ) from e jpayne@68: jpayne@68: jpayne@68: def _check_extra(extra, reqs): jpayne@68: name, sep, marker = extra.partition(':') jpayne@68: try: jpayne@68: _check_marker(marker) jpayne@68: except InvalidMarker: jpayne@68: msg = f"Invalid environment marker: {marker} ({extra!r})" jpayne@68: raise DistutilsSetupError(msg) from None jpayne@68: list(_reqs.parse(reqs)) jpayne@68: jpayne@68: jpayne@68: def _check_marker(marker): jpayne@68: if not marker: jpayne@68: return jpayne@68: m = Marker(marker) jpayne@68: m.evaluate() jpayne@68: jpayne@68: jpayne@68: def assert_bool(dist, attr, value): jpayne@68: """Verify that value is True, False, 0, or 1""" jpayne@68: if bool(value) != value: jpayne@68: raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})") jpayne@68: jpayne@68: jpayne@68: def invalid_unless_false(dist, attr, value): jpayne@68: if not value: jpayne@68: DistDeprecationWarning.emit(f"{attr} is ignored.") jpayne@68: # TODO: should there be a `due_date` here? jpayne@68: return jpayne@68: raise DistutilsSetupError(f"{attr} is invalid.") jpayne@68: jpayne@68: jpayne@68: def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None: jpayne@68: """Verify that install_requires is a valid requirements list""" jpayne@68: try: jpayne@68: list(_reqs.parse(value)) jpayne@68: if isinstance(value, set): jpayne@68: raise TypeError("Unordered types are not allowed") jpayne@68: except (TypeError, ValueError) as error: jpayne@68: msg = ( jpayne@68: f"{attr!r} must be a string or iterable of strings " jpayne@68: f"containing valid project/version requirement specifiers; {error}" jpayne@68: ) jpayne@68: raise DistutilsSetupError(msg) from error jpayne@68: jpayne@68: jpayne@68: def check_specifier(dist, attr, value): jpayne@68: """Verify that value is a valid version specifier""" jpayne@68: try: jpayne@68: SpecifierSet(value) jpayne@68: except (InvalidSpecifier, AttributeError) as error: jpayne@68: msg = f"{attr!r} must be a string containing valid version specifiers; {error}" jpayne@68: raise DistutilsSetupError(msg) from error jpayne@68: jpayne@68: jpayne@68: def check_entry_points(dist, attr, value): jpayne@68: """Verify that entry_points map is parseable""" jpayne@68: try: jpayne@68: _entry_points.load(value) jpayne@68: except Exception as e: jpayne@68: raise DistutilsSetupError(e) from e jpayne@68: jpayne@68: jpayne@68: def check_package_data(dist, attr, value): jpayne@68: """Verify that value is a dictionary of package names to glob lists""" jpayne@68: if not isinstance(value, dict): jpayne@68: raise DistutilsSetupError( jpayne@68: "{!r} must be a dictionary mapping package names to lists of " jpayne@68: "string wildcard patterns".format(attr) jpayne@68: ) jpayne@68: for k, v in value.items(): jpayne@68: if not isinstance(k, str): jpayne@68: raise DistutilsSetupError( jpayne@68: "keys of {!r} dict must be strings (got {!r})".format(attr, k) jpayne@68: ) jpayne@68: assert_string_list(dist, 'values of {!r} dict'.format(attr), v) jpayne@68: jpayne@68: jpayne@68: def check_packages(dist, attr, value): jpayne@68: for pkgname in value: jpayne@68: if not re.match(r'\w+(\.\w+)*', pkgname): jpayne@68: distutils.log.warn( jpayne@68: "WARNING: %r not a valid package name; please use only " jpayne@68: ".-separated package names in setup.py", jpayne@68: pkgname, jpayne@68: ) jpayne@68: jpayne@68: jpayne@68: if TYPE_CHECKING: jpayne@68: # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 jpayne@68: from distutils.core import Distribution as _Distribution jpayne@68: else: jpayne@68: _Distribution = get_unpatched(distutils.core.Distribution) jpayne@68: jpayne@68: jpayne@68: class Distribution(_Distribution): jpayne@68: """Distribution with support for tests and package data jpayne@68: jpayne@68: This is an enhanced version of 'distutils.dist.Distribution' that jpayne@68: effectively adds the following new optional keyword arguments to 'setup()': jpayne@68: jpayne@68: 'install_requires' -- a string or sequence of strings specifying project jpayne@68: versions that the distribution requires when installed, in the format jpayne@68: used by 'pkg_resources.require()'. They will be installed jpayne@68: automatically when the package is installed. If you wish to use jpayne@68: packages that are not available in PyPI, or want to give your users an jpayne@68: alternate download location, you can add a 'find_links' option to the jpayne@68: '[easy_install]' section of your project's 'setup.cfg' file, and then jpayne@68: setuptools will scan the listed web pages for links that satisfy the jpayne@68: requirements. jpayne@68: jpayne@68: 'extras_require' -- a dictionary mapping names of optional "extras" to the jpayne@68: additional requirement(s) that using those extras incurs. For example, jpayne@68: this:: jpayne@68: jpayne@68: extras_require = dict(reST = ["docutils>=0.3", "reSTedit"]) jpayne@68: jpayne@68: indicates that the distribution can optionally provide an extra jpayne@68: capability called "reST", but it can only be used if docutils and jpayne@68: reSTedit are installed. If the user installs your package using jpayne@68: EasyInstall and requests one of your extras, the corresponding jpayne@68: additional requirements will be installed if needed. jpayne@68: jpayne@68: 'package_data' -- a dictionary mapping package names to lists of filenames jpayne@68: or globs to use to find data files contained in the named packages. jpayne@68: If the dictionary has filenames or globs listed under '""' (the empty jpayne@68: string), those names will be searched for in every package, in addition jpayne@68: to any names for the specific package. Data files found using these jpayne@68: names/globs will be installed along with the package, in the same jpayne@68: location as the package. Note that globs are allowed to reference jpayne@68: the contents of non-package subdirectories, as long as you use '/' as jpayne@68: a path separator. (Globs are automatically converted to jpayne@68: platform-specific paths at runtime.) jpayne@68: jpayne@68: In addition to these new keywords, this class also has several new methods jpayne@68: for manipulating the distribution's contents. For example, the 'include()' jpayne@68: and 'exclude()' methods can be thought of as in-place add and subtract jpayne@68: commands that add or remove packages, modules, extensions, and so on from jpayne@68: the distribution. jpayne@68: """ jpayne@68: jpayne@68: _DISTUTILS_UNSUPPORTED_METADATA = { jpayne@68: 'long_description_content_type': lambda: None, jpayne@68: 'project_urls': dict, jpayne@68: 'provides_extras': dict, # behaves like an ordered set jpayne@68: 'license_file': lambda: None, jpayne@68: 'license_files': lambda: None, jpayne@68: 'install_requires': list, jpayne@68: 'extras_require': dict, jpayne@68: } jpayne@68: jpayne@68: # Used by build_py, editable_wheel and install_lib commands for legacy namespaces jpayne@68: namespace_packages: list[str] #: :meta private: DEPRECATED jpayne@68: jpayne@68: # Any: Dynamic assignment results in Incompatible types in assignment jpayne@68: def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None: jpayne@68: have_package_data = hasattr(self, "package_data") jpayne@68: if not have_package_data: jpayne@68: self.package_data: dict[str, list[str]] = {} jpayne@68: attrs = attrs or {} jpayne@68: self.dist_files: list[tuple[str, str, str]] = [] jpayne@68: self.include_package_data: bool | None = None jpayne@68: self.exclude_package_data: dict[str, list[str]] | None = None jpayne@68: # Filter-out setuptools' specific options. jpayne@68: self.src_root: str | None = attrs.pop("src_root", None) jpayne@68: self.dependency_links: list[str] = attrs.pop('dependency_links', []) jpayne@68: self.setup_requires: list[str] = attrs.pop('setup_requires', []) jpayne@68: for ep in metadata.entry_points(group='distutils.setup_keywords'): jpayne@68: vars(self).setdefault(ep.name, None) jpayne@68: jpayne@68: metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA) jpayne@68: metadata_only -= {"install_requires", "extras_require"} jpayne@68: dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} jpayne@68: _Distribution.__init__(self, dist_attrs) jpayne@68: jpayne@68: # Private API (setuptools-use only, not restricted to Distribution) jpayne@68: # Stores files that are referenced by the configuration and need to be in the jpayne@68: # sdist (e.g. `version = file: VERSION.txt`) jpayne@68: self._referenced_files: set[str] = set() jpayne@68: jpayne@68: self.set_defaults = ConfigDiscovery(self) jpayne@68: jpayne@68: self._set_metadata_defaults(attrs) jpayne@68: jpayne@68: self.metadata.version = self._normalize_version(self.metadata.version) jpayne@68: self._finalize_requires() jpayne@68: jpayne@68: def _validate_metadata(self): jpayne@68: required = {"name"} jpayne@68: provided = { jpayne@68: key jpayne@68: for key in vars(self.metadata) jpayne@68: if getattr(self.metadata, key, None) is not None jpayne@68: } jpayne@68: missing = required - provided jpayne@68: jpayne@68: if missing: jpayne@68: msg = f"Required package metadata is missing: {missing}" jpayne@68: raise DistutilsSetupError(msg) jpayne@68: jpayne@68: def _set_metadata_defaults(self, attrs): jpayne@68: """ jpayne@68: Fill-in missing metadata fields not supported by distutils. jpayne@68: Some fields may have been set by other tools (e.g. pbr). jpayne@68: Those fields (vars(self.metadata)) take precedence to jpayne@68: supplied attrs. jpayne@68: """ jpayne@68: for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items(): jpayne@68: vars(self.metadata).setdefault(option, attrs.get(option, default())) jpayne@68: jpayne@68: @staticmethod jpayne@68: def _normalize_version(version): jpayne@68: from . import sic jpayne@68: jpayne@68: if isinstance(version, numbers.Number): jpayne@68: # Some people apparently take "version number" too literally :) jpayne@68: version = str(version) jpayne@68: elif isinstance(version, sic) or version is None: jpayne@68: return version jpayne@68: jpayne@68: normalized = str(Version(version)) jpayne@68: if version != normalized: jpayne@68: InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'") jpayne@68: return normalized jpayne@68: return version jpayne@68: jpayne@68: def _finalize_requires(self): jpayne@68: """ jpayne@68: Set `metadata.python_requires` and fix environment markers jpayne@68: in `install_requires` and `extras_require`. jpayne@68: """ jpayne@68: if getattr(self, 'python_requires', None): jpayne@68: self.metadata.python_requires = self.python_requires jpayne@68: jpayne@68: self._normalize_requires() jpayne@68: self.metadata.install_requires = self.install_requires jpayne@68: self.metadata.extras_require = self.extras_require jpayne@68: jpayne@68: if self.extras_require: jpayne@68: for extra in self.extras_require.keys(): jpayne@68: # Setuptools allows a weird ": syntax for extras jpayne@68: extra = extra.split(':')[0] jpayne@68: if extra: jpayne@68: self.metadata.provides_extras.setdefault(extra) jpayne@68: jpayne@68: def _normalize_requires(self): jpayne@68: """Make sure requirement-related attributes exist and are normalized""" jpayne@68: install_requires = getattr(self, "install_requires", None) or [] jpayne@68: extras_require = getattr(self, "extras_require", None) or {} jpayne@68: self.install_requires = list(map(str, _reqs.parse(install_requires))) jpayne@68: self.extras_require = { jpayne@68: k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items() jpayne@68: } jpayne@68: jpayne@68: def _finalize_license_files(self) -> None: jpayne@68: """Compute names of all license files which should be included.""" jpayne@68: license_files: list[str] | None = self.metadata.license_files jpayne@68: patterns: list[str] = license_files if license_files else [] jpayne@68: jpayne@68: license_file: str | None = self.metadata.license_file jpayne@68: if license_file and license_file not in patterns: jpayne@68: patterns.append(license_file) jpayne@68: jpayne@68: if license_files is None and license_file is None: jpayne@68: # Default patterns match the ones wheel uses jpayne@68: # See https://wheel.readthedocs.io/en/stable/user_guide.html jpayne@68: # -> 'Including license files in the generated wheel file' jpayne@68: patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] jpayne@68: jpayne@68: self.metadata.license_files = list( jpayne@68: unique_everseen(self._expand_patterns(patterns)) jpayne@68: ) jpayne@68: jpayne@68: @staticmethod jpayne@68: def _expand_patterns(patterns): jpayne@68: """ jpayne@68: >>> list(Distribution._expand_patterns(['LICENSE'])) jpayne@68: ['LICENSE'] jpayne@68: >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*'])) jpayne@68: ['pyproject.toml', 'LICENSE'] jpayne@68: """ jpayne@68: return ( jpayne@68: path jpayne@68: for pattern in patterns jpayne@68: for path in sorted(iglob(pattern)) jpayne@68: if not path.endswith('~') and os.path.isfile(path) jpayne@68: ) jpayne@68: jpayne@68: # FIXME: 'Distribution._parse_config_files' is too complex (14) jpayne@68: def _parse_config_files(self, filenames=None): # noqa: C901 jpayne@68: """ jpayne@68: Adapted from distutils.dist.Distribution.parse_config_files, jpayne@68: this method provides the same functionality in subtly-improved jpayne@68: ways. jpayne@68: """ jpayne@68: from configparser import ConfigParser jpayne@68: jpayne@68: # Ignore install directory options if we have a venv jpayne@68: ignore_options = ( jpayne@68: [] jpayne@68: if sys.prefix == sys.base_prefix jpayne@68: else [ jpayne@68: 'install-base', jpayne@68: 'install-platbase', jpayne@68: 'install-lib', jpayne@68: 'install-platlib', jpayne@68: 'install-purelib', jpayne@68: 'install-headers', jpayne@68: 'install-scripts', jpayne@68: 'install-data', jpayne@68: 'prefix', jpayne@68: 'exec-prefix', jpayne@68: 'home', jpayne@68: 'user', jpayne@68: 'root', jpayne@68: ] jpayne@68: ) jpayne@68: jpayne@68: ignore_options = frozenset(ignore_options) jpayne@68: jpayne@68: if filenames is None: jpayne@68: filenames = self.find_config_files() jpayne@68: jpayne@68: if DEBUG: jpayne@68: self.announce("Distribution.parse_config_files():") jpayne@68: jpayne@68: parser = ConfigParser() jpayne@68: parser.optionxform = str jpayne@68: for filename in filenames: jpayne@68: with open(filename, encoding='utf-8') as reader: jpayne@68: if DEBUG: jpayne@68: self.announce(" reading {filename}".format(**locals())) jpayne@68: parser.read_file(reader) jpayne@68: for section in parser.sections(): jpayne@68: options = parser.options(section) jpayne@68: opt_dict = self.get_option_dict(section) jpayne@68: jpayne@68: for opt in options: jpayne@68: if opt == '__name__' or opt in ignore_options: jpayne@68: continue jpayne@68: jpayne@68: val = parser.get(section, opt) jpayne@68: opt = self.warn_dash_deprecation(opt, section) jpayne@68: opt = self.make_option_lowercase(opt, section) jpayne@68: opt_dict[opt] = (filename, val) jpayne@68: jpayne@68: # Make the ConfigParser forget everything (so we retain jpayne@68: # the original filenames that options come from) jpayne@68: parser.__init__() jpayne@68: jpayne@68: if 'global' not in self.command_options: jpayne@68: return jpayne@68: jpayne@68: # If there was a "global" section in the config file, use it jpayne@68: # to set Distribution options. jpayne@68: jpayne@68: for opt, (src, val) in self.command_options['global'].items(): jpayne@68: alias = self.negative_opt.get(opt) jpayne@68: if alias: jpayne@68: val = not strtobool(val) jpayne@68: elif opt in ('verbose', 'dry_run'): # ugh! jpayne@68: val = strtobool(val) jpayne@68: jpayne@68: try: jpayne@68: setattr(self, alias or opt, val) jpayne@68: except ValueError as e: jpayne@68: raise DistutilsOptionError(e) from e jpayne@68: jpayne@68: def warn_dash_deprecation(self, opt: str, section: str): jpayne@68: if section in ( jpayne@68: 'options.extras_require', jpayne@68: 'options.data_files', jpayne@68: ): jpayne@68: return opt jpayne@68: jpayne@68: underscore_opt = opt.replace('-', '_') jpayne@68: commands = list( jpayne@68: itertools.chain( jpayne@68: distutils.command.__all__, jpayne@68: self._setuptools_commands(), jpayne@68: ) jpayne@68: ) jpayne@68: if ( jpayne@68: not section.startswith('options') jpayne@68: and section != 'metadata' jpayne@68: and section not in commands jpayne@68: ): jpayne@68: return underscore_opt jpayne@68: jpayne@68: if '-' in opt: jpayne@68: SetuptoolsDeprecationWarning.emit( jpayne@68: "Invalid dash-separated options", jpayne@68: f""" jpayne@68: Usage of dash-separated {opt!r} will not be supported in future jpayne@68: versions. Please use the underscore name {underscore_opt!r} instead. jpayne@68: """, jpayne@68: see_docs="userguide/declarative_config.html", jpayne@68: due_date=(2025, 3, 3), jpayne@68: # Warning initially introduced in 3 Mar 2021 jpayne@68: ) jpayne@68: return underscore_opt jpayne@68: jpayne@68: def _setuptools_commands(self): jpayne@68: try: jpayne@68: entry_points = metadata.distribution('setuptools').entry_points jpayne@68: return {ep.name for ep in entry_points} # Avoid newer API for compatibility jpayne@68: except metadata.PackageNotFoundError: jpayne@68: # during bootstrapping, distribution doesn't exist jpayne@68: return [] jpayne@68: jpayne@68: def make_option_lowercase(self, opt: str, section: str): jpayne@68: if section != 'metadata' or opt.islower(): jpayne@68: return opt jpayne@68: jpayne@68: lowercase_opt = opt.lower() jpayne@68: SetuptoolsDeprecationWarning.emit( jpayne@68: "Invalid uppercase configuration", jpayne@68: f""" jpayne@68: Usage of uppercase key {opt!r} in {section!r} will not be supported in jpayne@68: future versions. Please use lowercase {lowercase_opt!r} instead. jpayne@68: """, jpayne@68: see_docs="userguide/declarative_config.html", jpayne@68: due_date=(2025, 3, 3), jpayne@68: # Warning initially introduced in 6 Mar 2021 jpayne@68: ) jpayne@68: return lowercase_opt jpayne@68: jpayne@68: # FIXME: 'Distribution._set_command_options' is too complex (14) jpayne@68: def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 jpayne@68: """ jpayne@68: Set the options for 'command_obj' from 'option_dict'. Basically jpayne@68: this means copying elements of a dictionary ('option_dict') to jpayne@68: attributes of an instance ('command'). jpayne@68: jpayne@68: 'command_obj' must be a Command instance. If 'option_dict' is not jpayne@68: supplied, uses the standard option dictionary for this command jpayne@68: (from 'self.command_options'). jpayne@68: jpayne@68: (Adopted from distutils.dist.Distribution._set_command_options) jpayne@68: """ jpayne@68: command_name = command_obj.get_command_name() jpayne@68: if option_dict is None: jpayne@68: option_dict = self.get_option_dict(command_name) jpayne@68: jpayne@68: if DEBUG: jpayne@68: self.announce(" setting options for '%s' command:" % command_name) jpayne@68: for option, (source, value) in option_dict.items(): jpayne@68: if DEBUG: jpayne@68: self.announce(" %s = %s (from %s)" % (option, value, source)) jpayne@68: try: jpayne@68: bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] jpayne@68: except AttributeError: jpayne@68: bool_opts = [] jpayne@68: try: jpayne@68: neg_opt = command_obj.negative_opt jpayne@68: except AttributeError: jpayne@68: neg_opt = {} jpayne@68: jpayne@68: try: jpayne@68: is_string = isinstance(value, str) jpayne@68: if option in neg_opt and is_string: jpayne@68: setattr(command_obj, neg_opt[option], not strtobool(value)) jpayne@68: elif option in bool_opts and is_string: jpayne@68: setattr(command_obj, option, strtobool(value)) jpayne@68: elif hasattr(command_obj, option): jpayne@68: setattr(command_obj, option, value) jpayne@68: else: jpayne@68: raise DistutilsOptionError( jpayne@68: "error in %s: command '%s' has no such option '%s'" jpayne@68: % (source, command_name, option) jpayne@68: ) jpayne@68: except ValueError as e: jpayne@68: raise DistutilsOptionError(e) from e jpayne@68: jpayne@68: def _get_project_config_files(self, filenames: Iterable[StrPath] | None): jpayne@68: """Add default file and split between INI and TOML""" jpayne@68: tomlfiles = [] jpayne@68: standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml") jpayne@68: if filenames is not None: jpayne@68: parts = partition(lambda f: Path(f).suffix == ".toml", filenames) jpayne@68: filenames = list(parts[0]) # 1st element => predicate is False jpayne@68: tomlfiles = list(parts[1]) # 2nd element => predicate is True jpayne@68: elif standard_project_metadata.exists(): jpayne@68: tomlfiles = [standard_project_metadata] jpayne@68: return filenames, tomlfiles jpayne@68: jpayne@68: def parse_config_files( jpayne@68: self, jpayne@68: filenames: Iterable[StrPath] | None = None, jpayne@68: ignore_option_errors: bool = False, jpayne@68: ): jpayne@68: """Parses configuration files from various levels jpayne@68: and loads configuration. jpayne@68: """ jpayne@68: inifiles, tomlfiles = self._get_project_config_files(filenames) jpayne@68: jpayne@68: self._parse_config_files(filenames=inifiles) jpayne@68: jpayne@68: setupcfg.parse_configuration( jpayne@68: self, self.command_options, ignore_option_errors=ignore_option_errors jpayne@68: ) jpayne@68: for filename in tomlfiles: jpayne@68: pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) jpayne@68: jpayne@68: self._finalize_requires() jpayne@68: self._finalize_license_files() jpayne@68: jpayne@68: def fetch_build_eggs(self, requires: _StrOrIter): jpayne@68: """Resolve pre-setup requirements""" jpayne@68: from .installer import _fetch_build_eggs jpayne@68: jpayne@68: return _fetch_build_eggs(self, requires) jpayne@68: jpayne@68: def finalize_options(self): jpayne@68: """ jpayne@68: Allow plugins to apply arbitrary operations to the jpayne@68: distribution. Each hook may optionally define a 'order' jpayne@68: to influence the order of execution. Smaller numbers jpayne@68: go first and the default is 0. jpayne@68: """ jpayne@68: group = 'setuptools.finalize_distribution_options' jpayne@68: jpayne@68: def by_order(hook): jpayne@68: return getattr(hook, 'order', 0) jpayne@68: jpayne@68: defined = metadata.entry_points(group=group) jpayne@68: filtered = itertools.filterfalse(self._removed, defined) jpayne@68: loaded = map(lambda e: e.load(), filtered) jpayne@68: for ep in sorted(loaded, key=by_order): jpayne@68: ep(self) jpayne@68: jpayne@68: @staticmethod jpayne@68: def _removed(ep): jpayne@68: """ jpayne@68: When removing an entry point, if metadata is loaded jpayne@68: from an older version of Setuptools, that removed jpayne@68: entry point will attempt to be loaded and will fail. jpayne@68: See #2765 for more details. jpayne@68: """ jpayne@68: removed = { jpayne@68: # removed 2021-09-05 jpayne@68: '2to3_doctests', jpayne@68: } jpayne@68: return ep.name in removed jpayne@68: jpayne@68: def _finalize_setup_keywords(self): jpayne@68: for ep in metadata.entry_points(group='distutils.setup_keywords'): jpayne@68: value = getattr(self, ep.name, None) jpayne@68: if value is not None: jpayne@68: ep.load()(self, ep.name, value) jpayne@68: jpayne@68: def get_egg_cache_dir(self): jpayne@68: from . import windows_support jpayne@68: jpayne@68: egg_cache_dir = os.path.join(os.curdir, '.eggs') jpayne@68: if not os.path.exists(egg_cache_dir): jpayne@68: os.mkdir(egg_cache_dir) jpayne@68: windows_support.hide_file(egg_cache_dir) jpayne@68: readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt') jpayne@68: with open(readme_txt_filename, 'w', encoding="utf-8") as f: jpayne@68: f.write( jpayne@68: 'This directory contains eggs that were downloaded ' jpayne@68: 'by setuptools to build, test, and run plug-ins.\n\n' jpayne@68: ) jpayne@68: f.write( jpayne@68: 'This directory caches those eggs to prevent ' jpayne@68: 'repeated downloads.\n\n' jpayne@68: ) jpayne@68: f.write('However, it is safe to delete this directory.\n\n') jpayne@68: jpayne@68: return egg_cache_dir jpayne@68: jpayne@68: def fetch_build_egg(self, req): jpayne@68: """Fetch an egg needed for building""" jpayne@68: from .installer import fetch_build_egg jpayne@68: jpayne@68: return fetch_build_egg(self, req) jpayne@68: jpayne@68: def get_command_class(self, command: str): jpayne@68: """Pluggable version of get_command_class()""" jpayne@68: if command in self.cmdclass: jpayne@68: return self.cmdclass[command] jpayne@68: jpayne@68: # Special case bdist_wheel so it's never loaded from "wheel" jpayne@68: if command == 'bdist_wheel': jpayne@68: from .command.bdist_wheel import bdist_wheel jpayne@68: jpayne@68: return bdist_wheel jpayne@68: jpayne@68: eps = metadata.entry_points(group='distutils.commands', name=command) jpayne@68: for ep in eps: jpayne@68: self.cmdclass[command] = cmdclass = ep.load() jpayne@68: return cmdclass jpayne@68: else: jpayne@68: return _Distribution.get_command_class(self, command) jpayne@68: jpayne@68: def print_commands(self): jpayne@68: for ep in metadata.entry_points(group='distutils.commands'): jpayne@68: if ep.name not in self.cmdclass: jpayne@68: cmdclass = ep.load() jpayne@68: self.cmdclass[ep.name] = cmdclass jpayne@68: return _Distribution.print_commands(self) jpayne@68: jpayne@68: def get_command_list(self): jpayne@68: for ep in metadata.entry_points(group='distutils.commands'): jpayne@68: if ep.name not in self.cmdclass: jpayne@68: cmdclass = ep.load() jpayne@68: self.cmdclass[ep.name] = cmdclass jpayne@68: return _Distribution.get_command_list(self) jpayne@68: jpayne@68: def include(self, **attrs): jpayne@68: """Add items to distribution that are named in keyword arguments jpayne@68: jpayne@68: For example, 'dist.include(py_modules=["x"])' would add 'x' to jpayne@68: the distribution's 'py_modules' attribute, if it was not already jpayne@68: there. jpayne@68: jpayne@68: Currently, this method only supports inclusion for attributes that are jpayne@68: lists or tuples. If you need to add support for adding to other jpayne@68: attributes in this or a subclass, you can add an '_include_X' method, jpayne@68: where 'X' is the name of the attribute. The method will be called with jpayne@68: the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})' jpayne@68: will try to call 'dist._include_foo({"bar":"baz"})', which can then jpayne@68: handle whatever special inclusion logic is needed. jpayne@68: """ jpayne@68: for k, v in attrs.items(): jpayne@68: include = getattr(self, '_include_' + k, None) jpayne@68: if include: jpayne@68: include(v) jpayne@68: else: jpayne@68: self._include_misc(k, v) jpayne@68: jpayne@68: def exclude_package(self, package: str): jpayne@68: """Remove packages, modules, and extensions in named package""" jpayne@68: jpayne@68: pfx = package + '.' jpayne@68: if self.packages: jpayne@68: self.packages = [ jpayne@68: p for p in self.packages if p != package and not p.startswith(pfx) jpayne@68: ] jpayne@68: jpayne@68: if self.py_modules: jpayne@68: self.py_modules = [ jpayne@68: p for p in self.py_modules if p != package and not p.startswith(pfx) jpayne@68: ] jpayne@68: jpayne@68: if self.ext_modules: jpayne@68: self.ext_modules = [ jpayne@68: p jpayne@68: for p in self.ext_modules jpayne@68: if p.name != package and not p.name.startswith(pfx) jpayne@68: ] jpayne@68: jpayne@68: def has_contents_for(self, package: str): jpayne@68: """Return true if 'exclude_package(package)' would do something""" jpayne@68: jpayne@68: pfx = package + '.' jpayne@68: jpayne@68: for p in self.iter_distribution_names(): jpayne@68: if p == package or p.startswith(pfx): jpayne@68: return True jpayne@68: jpayne@68: return False jpayne@68: jpayne@68: def _exclude_misc(self, name: str, value: _Sequence) -> None: jpayne@68: """Handle 'exclude()' for list/tuple attrs without a special handler""" jpayne@68: if not isinstance(value, _sequence): jpayne@68: raise DistutilsSetupError( jpayne@68: f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" jpayne@68: ) jpayne@68: try: jpayne@68: old = getattr(self, name) jpayne@68: except AttributeError as e: jpayne@68: raise DistutilsSetupError("%s: No such distribution setting" % name) from e jpayne@68: if old is not None and not isinstance(old, _sequence): jpayne@68: raise DistutilsSetupError( jpayne@68: name + ": this setting cannot be changed via include/exclude" jpayne@68: ) jpayne@68: elif old: jpayne@68: setattr(self, name, [item for item in old if item not in value]) jpayne@68: jpayne@68: def _include_misc(self, name: str, value: _Sequence) -> None: jpayne@68: """Handle 'include()' for list/tuple attrs without a special handler""" jpayne@68: jpayne@68: if not isinstance(value, _sequence): jpayne@68: raise DistutilsSetupError( jpayne@68: f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})" jpayne@68: ) jpayne@68: try: jpayne@68: old = getattr(self, name) jpayne@68: except AttributeError as e: jpayne@68: raise DistutilsSetupError("%s: No such distribution setting" % name) from e jpayne@68: if old is None: jpayne@68: setattr(self, name, value) jpayne@68: elif not isinstance(old, _sequence): jpayne@68: raise DistutilsSetupError( jpayne@68: name + ": this setting cannot be changed via include/exclude" jpayne@68: ) jpayne@68: else: jpayne@68: new = [item for item in value if item not in old] jpayne@68: setattr(self, name, list(old) + new) jpayne@68: jpayne@68: def exclude(self, **attrs): jpayne@68: """Remove items from distribution that are named in keyword arguments jpayne@68: jpayne@68: For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from jpayne@68: the distribution's 'py_modules' attribute. Excluding packages uses jpayne@68: the 'exclude_package()' method, so all of the package's contained jpayne@68: packages, modules, and extensions are also excluded. jpayne@68: jpayne@68: Currently, this method only supports exclusion from attributes that are jpayne@68: lists or tuples. If you need to add support for excluding from other jpayne@68: attributes in this or a subclass, you can add an '_exclude_X' method, jpayne@68: where 'X' is the name of the attribute. The method will be called with jpayne@68: the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})' jpayne@68: will try to call 'dist._exclude_foo({"bar":"baz"})', which can then jpayne@68: handle whatever special exclusion logic is needed. jpayne@68: """ jpayne@68: for k, v in attrs.items(): jpayne@68: exclude = getattr(self, '_exclude_' + k, None) jpayne@68: if exclude: jpayne@68: exclude(v) jpayne@68: else: jpayne@68: self._exclude_misc(k, v) jpayne@68: jpayne@68: def _exclude_packages(self, packages: _Sequence) -> None: jpayne@68: if not isinstance(packages, _sequence): jpayne@68: raise DistutilsSetupError( jpayne@68: f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})" jpayne@68: ) jpayne@68: list(map(self.exclude_package, packages)) jpayne@68: jpayne@68: def _parse_command_opts(self, parser, args): jpayne@68: # Remove --with-X/--without-X options when processing command args jpayne@68: self.global_options = self.__class__.global_options jpayne@68: self.negative_opt = self.__class__.negative_opt jpayne@68: jpayne@68: # First, expand any aliases jpayne@68: command = args[0] jpayne@68: aliases = self.get_option_dict('aliases') jpayne@68: while command in aliases: jpayne@68: src, alias = aliases[command] jpayne@68: del aliases[command] # ensure each alias can expand only once! jpayne@68: import shlex jpayne@68: jpayne@68: args[:1] = shlex.split(alias, True) jpayne@68: command = args[0] jpayne@68: jpayne@68: nargs = _Distribution._parse_command_opts(self, parser, args) jpayne@68: jpayne@68: # Handle commands that want to consume all remaining arguments jpayne@68: cmd_class = self.get_command_class(command) jpayne@68: if getattr(cmd_class, 'command_consumes_arguments', None): jpayne@68: self.get_option_dict(command)['args'] = ("command line", nargs) jpayne@68: if nargs is not None: jpayne@68: return [] jpayne@68: jpayne@68: return nargs jpayne@68: jpayne@68: def get_cmdline_options(self): jpayne@68: """Return a '{cmd: {opt:val}}' map of all command-line options jpayne@68: jpayne@68: Option names are all long, but do not include the leading '--', and jpayne@68: contain dashes rather than underscores. If the option doesn't take jpayne@68: an argument (e.g. '--quiet'), the 'val' is 'None'. jpayne@68: jpayne@68: Note that options provided by config files are intentionally excluded. jpayne@68: """ jpayne@68: jpayne@68: d = {} jpayne@68: jpayne@68: for cmd, opts in self.command_options.items(): jpayne@68: for opt, (src, val) in opts.items(): jpayne@68: if src != "command line": jpayne@68: continue jpayne@68: jpayne@68: opt = opt.replace('_', '-') jpayne@68: jpayne@68: if val == 0: jpayne@68: cmdobj = self.get_command_obj(cmd) jpayne@68: neg_opt = self.negative_opt.copy() jpayne@68: neg_opt.update(getattr(cmdobj, 'negative_opt', {})) jpayne@68: for neg, pos in neg_opt.items(): jpayne@68: if pos == opt: jpayne@68: opt = neg jpayne@68: val = None jpayne@68: break jpayne@68: else: jpayne@68: raise AssertionError("Shouldn't be able to get here") jpayne@68: jpayne@68: elif val == 1: jpayne@68: val = None jpayne@68: jpayne@68: d.setdefault(cmd, {})[opt] = val jpayne@68: jpayne@68: return d jpayne@68: jpayne@68: def iter_distribution_names(self): jpayne@68: """Yield all packages, modules, and extension names in distribution""" jpayne@68: jpayne@68: yield from self.packages or () jpayne@68: jpayne@68: yield from self.py_modules or () jpayne@68: jpayne@68: for ext in self.ext_modules or (): jpayne@68: if isinstance(ext, tuple): jpayne@68: name, buildinfo = ext jpayne@68: else: jpayne@68: name = ext.name jpayne@68: if name.endswith('module'): jpayne@68: name = name[:-6] jpayne@68: yield name jpayne@68: jpayne@68: def handle_display_options(self, option_order): jpayne@68: """If there were any non-global "display-only" options jpayne@68: (--help-commands or the metadata display options) on the command jpayne@68: line, display the requested info and return true; else return jpayne@68: false. jpayne@68: """ jpayne@68: import sys jpayne@68: jpayne@68: if self.help_commands: jpayne@68: return _Distribution.handle_display_options(self, option_order) jpayne@68: jpayne@68: # Stdout may be StringIO (e.g. in tests) jpayne@68: if not isinstance(sys.stdout, io.TextIOWrapper): jpayne@68: return _Distribution.handle_display_options(self, option_order) jpayne@68: jpayne@68: # Don't wrap stdout if utf-8 is already the encoding. Provides jpayne@68: # workaround for #334. jpayne@68: if sys.stdout.encoding.lower() in ('utf-8', 'utf8'): jpayne@68: return _Distribution.handle_display_options(self, option_order) jpayne@68: jpayne@68: # Print metadata in UTF-8 no matter the platform jpayne@68: encoding = sys.stdout.encoding jpayne@68: sys.stdout.reconfigure(encoding='utf-8') jpayne@68: try: jpayne@68: return _Distribution.handle_display_options(self, option_order) jpayne@68: finally: jpayne@68: sys.stdout.reconfigure(encoding=encoding) jpayne@68: jpayne@68: def run_command(self, command): jpayne@68: self.set_defaults() jpayne@68: # Postpone defaults until all explicit configuration is considered jpayne@68: # (setup() args, config files, command line and plugins) jpayne@68: jpayne@68: super().run_command(command) jpayne@68: jpayne@68: jpayne@68: class DistDeprecationWarning(SetuptoolsDeprecationWarning): jpayne@68: """Class for warning about deprecations in dist in jpayne@68: setuptools. Not ignored by default, unlike DeprecationWarning."""