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