codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import functools4import io5import itertools6import numbers7import os8import re9import sys10from collections.abc import Iterable, Iterator, MutableMapping, Sequence11from glob import glob12from pathlib import Path13from typing import TYPE_CHECKING, Any, Union14 15from more_itertools import partition, unique_everseen16from packaging.markers import InvalidMarker, Marker17from packaging.specifiers import InvalidSpecifier, SpecifierSet18from packaging.version import Version19 20from . import (21 _entry_points,22 _reqs,23 _static,24 command as _, # noqa: F401 # imported for side-effects25)26from ._importlib import metadata27from ._normalization import _canonicalize_license_expression28from ._path import StrPath29from ._reqs import _StrOrIter30from .config import pyprojecttoml, setupcfg31from .discovery import ConfigDiscovery32from .errors import InvalidConfigError33from .monkey import get_unpatched34from .warnings import InformationOnly, SetuptoolsDeprecationWarning35 36import distutils.cmd37import distutils.command38import distutils.core39import distutils.dist40import distutils.log41from distutils.debug import DEBUG42from distutils.errors import DistutilsOptionError, DistutilsSetupError43from distutils.fancy_getopt import translate_longopt44from distutils.util import strtobool45 46if TYPE_CHECKING:47 from typing_extensions import TypeAlias48 49 50__all__ = ['Distribution']51 52_sequence = tuple, list53"""54:meta private:55 56Supported iterable types that are known to be:57- ordered (which `set` isn't)58- not match a str (which `Sequence[str]` does)59- not imply a nested type (like `dict`)60for use with `isinstance`.61"""62_Sequence: TypeAlias = Union[tuple[str, ...], list[str]]63# This is how stringifying _Sequence would look in Python 3.1064_sequence_type_repr = "tuple[str, ...] | list[str]"65_OrderedStrSequence: TypeAlias = Union[str, dict[str, Any], Sequence[str]]66"""67:meta private:68Avoid single-use iterable. Disallow sets.69A poor approximation of an OrderedSequence (dict doesn't match a Sequence).70"""71 72 73def __getattr__(name: str) -> Any: # pragma: no cover74 if name == "sequence":75 SetuptoolsDeprecationWarning.emit(76 "`setuptools.dist.sequence` is an internal implementation detail.",77 "Please define your own `sequence = tuple, list` instead.",78 due_date=(2025, 8, 28), # Originally added on 2024-08-2779 )80 return _sequence81 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")82 83 84def check_importable(dist, attr, value):85 try:86 ep = metadata.EntryPoint(value=value, name=None, group=None)87 assert not ep.extras88 except (TypeError, ValueError, AttributeError, AssertionError) as e:89 raise DistutilsSetupError(90 f"{attr!r} must be importable 'module:attrs' string (got {value!r})"91 ) from e92 93 94def assert_string_list(dist, attr: str, value: _Sequence) -> None:95 """Verify that value is a string list"""96 try:97 # verify that value is a list or tuple to exclude unordered98 # or single-use iterables99 assert isinstance(value, _sequence)100 # verify that elements of value are strings101 assert ''.join(value) != value102 except (TypeError, ValueError, AttributeError, AssertionError) as e:103 raise DistutilsSetupError(104 f"{attr!r} must be of type <{_sequence_type_repr}> (got {value!r})"105 ) from e106 107 108def check_nsp(dist, attr, value):109 """Verify that namespace packages are valid"""110 ns_packages = value111 assert_string_list(dist, attr, ns_packages)112 for nsp in ns_packages:113 if not dist.has_contents_for(nsp):114 raise DistutilsSetupError(115 f"Distribution contains no modules or packages for namespace package {nsp!r}"116 )117 parent, _sep, _child = nsp.rpartition('.')118 if parent and parent not in ns_packages:119 distutils.log.warn(120 "WARNING: %r is declared as a package namespace, but %r"121 " is not: please correct this in setup.py",122 nsp,123 parent,124 )125 SetuptoolsDeprecationWarning.emit(126 "The namespace_packages parameter is deprecated.",127 "Please replace its usage with implicit namespaces (PEP 420).",128 see_docs="references/keywords.html#keyword-namespace-packages",129 # TODO: define due_date, it may break old packages that are no longer130 # maintained (e.g. sphinxcontrib extensions) when installed from source.131 # Warning officially introduced in May 2022, however the deprecation132 # was mentioned much earlier in the docs (May 2020, see #2149).133 )134 135 136def check_extras(dist, attr, value):137 """Verify that extras_require mapping is valid"""138 try:139 list(itertools.starmap(_check_extra, value.items()))140 except (TypeError, ValueError, AttributeError) as e:141 raise DistutilsSetupError(142 "'extras_require' must be a dictionary whose values are "143 "strings or lists of strings containing valid project/version "144 "requirement specifiers."145 ) from e146 147 148def _check_extra(extra, reqs):149 _name, _sep, marker = extra.partition(':')150 try:151 _check_marker(marker)152 except InvalidMarker:153 msg = f"Invalid environment marker: {marker} ({extra!r})"154 raise DistutilsSetupError(msg) from None155 list(_reqs.parse(reqs))156 157 158def _check_marker(marker):159 if not marker:160 return161 m = Marker(marker)162 m.evaluate()163 164 165def assert_bool(dist, attr, value):166 """Verify that value is True, False, 0, or 1"""167 if bool(value) != value:168 raise DistutilsSetupError(f"{attr!r} must be a boolean value (got {value!r})")169 170 171def invalid_unless_false(dist, attr, value):172 if not value:173 DistDeprecationWarning.emit(f"{attr} is ignored.")174 # TODO: should there be a `due_date` here?175 return176 raise DistutilsSetupError(f"{attr} is invalid.")177 178 179def check_requirements(dist, attr: str, value: _OrderedStrSequence) -> None:180 """Verify that install_requires is a valid requirements list"""181 try:182 list(_reqs.parse(value))183 if isinstance(value, set):184 raise TypeError("Unordered types are not allowed")185 except (TypeError, ValueError) as error:186 msg = (187 f"{attr!r} must be a string or iterable of strings "188 f"containing valid project/version requirement specifiers; {error}"189 )190 raise DistutilsSetupError(msg) from error191 192 193def check_specifier(dist, attr, value):194 """Verify that value is a valid version specifier"""195 try:196 SpecifierSet(value)197 except (InvalidSpecifier, AttributeError) as error:198 msg = f"{attr!r} must be a string containing valid version specifiers; {error}"199 raise DistutilsSetupError(msg) from error200 201 202def check_entry_points(dist, attr, value):203 """Verify that entry_points map is parseable"""204 try:205 _entry_points.load(value)206 except Exception as e:207 raise DistutilsSetupError(e) from e208 209 210def check_package_data(dist, attr, value):211 """Verify that value is a dictionary of package names to glob lists"""212 if not isinstance(value, dict):213 raise DistutilsSetupError(214 f"{attr!r} must be a dictionary mapping package names to lists of "215 "string wildcard patterns"216 )217 for k, v in value.items():218 if not isinstance(k, str):219 raise DistutilsSetupError(220 f"keys of {attr!r} dict must be strings (got {k!r})"221 )222 assert_string_list(dist, f'values of {attr!r} dict', v)223 224 225def check_packages(dist, attr, value):226 for pkgname in value:227 if not re.match(r'\w+(\.\w+)*', pkgname):228 distutils.log.warn(229 "WARNING: %r not a valid package name; please use only "230 ".-separated package names in setup.py",231 pkgname,232 )233 234 235if TYPE_CHECKING:236 # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962237 from distutils.core import Distribution as _Distribution238else:239 _Distribution = get_unpatched(distutils.core.Distribution)240 241 242class Distribution(_Distribution):243 """Distribution with support for tests and package data244 245 This is an enhanced version of 'distutils.dist.Distribution' that246 effectively adds the following new optional keyword arguments to 'setup()':247 248 'install_requires' -- a string or sequence of strings specifying project249 versions that the distribution requires when installed, in the format250 used by 'pkg_resources.require()'. They will be installed251 automatically when the package is installed. If you wish to use252 packages that are not available in PyPI, or want to give your users an253 alternate download location, you can add a 'find_links' option to the254 '[easy_install]' section of your project's 'setup.cfg' file, and then255 setuptools will scan the listed web pages for links that satisfy the256 requirements.257 258 'extras_require' -- a dictionary mapping names of optional "extras" to the259 additional requirement(s) that using those extras incurs. For example,260 this::261 262 extras_require = dict(reST = ["docutils>=0.3", "reSTedit"])263 264 indicates that the distribution can optionally provide an extra265 capability called "reST", but it can only be used if docutils and266 reSTedit are installed. If the user installs your package using267 EasyInstall and requests one of your extras, the corresponding268 additional requirements will be installed if needed.269 270 'package_data' -- a dictionary mapping package names to lists of filenames271 or globs to use to find data files contained in the named packages.272 If the dictionary has filenames or globs listed under '""' (the empty273 string), those names will be searched for in every package, in addition274 to any names for the specific package. Data files found using these275 names/globs will be installed along with the package, in the same276 location as the package. Note that globs are allowed to reference277 the contents of non-package subdirectories, as long as you use '/' as278 a path separator. (Globs are automatically converted to279 platform-specific paths at runtime.)280 281 In addition to these new keywords, this class also has several new methods282 for manipulating the distribution's contents. For example, the 'include()'283 and 'exclude()' methods can be thought of as in-place add and subtract284 commands that add or remove packages, modules, extensions, and so on from285 the distribution.286 """287 288 _DISTUTILS_UNSUPPORTED_METADATA = {289 'long_description_content_type': lambda: None,290 'project_urls': dict,291 'provides_extras': dict, # behaves like an ordered set292 'license_expression': lambda: None,293 'license_file': lambda: None,294 'license_files': lambda: None,295 'install_requires': list,296 'extras_require': dict,297 }298 299 # Used by build_py, editable_wheel and install_lib commands for legacy namespaces300 namespace_packages: list[str] #: :meta private: DEPRECATED301 302 # Any: Dynamic assignment results in Incompatible types in assignment303 def __init__(self, attrs: MutableMapping[str, Any] | None = None) -> None:304 have_package_data = hasattr(self, "package_data")305 if not have_package_data:306 self.package_data: dict[str, list[str]] = {}307 attrs = attrs or {}308 self.dist_files: list[tuple[str, str, str]] = []309 self.include_package_data: bool | None = None310 self.exclude_package_data: dict[str, list[str]] | None = None311 # Filter-out setuptools' specific options.312 self.src_root: str | None = attrs.pop("src_root", None)313 self.dependency_links: list[str] = attrs.pop('dependency_links', [])314 self.setup_requires: list[str] = attrs.pop('setup_requires', [])315 for ep in metadata.entry_points(group='distutils.setup_keywords'):316 vars(self).setdefault(ep.name, None)317 318 metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA)319 metadata_only -= {"install_requires", "extras_require"}320 dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only}321 _Distribution.__init__(self, dist_attrs)322 323 # Private API (setuptools-use only, not restricted to Distribution)324 # Stores files that are referenced by the configuration and need to be in the325 # sdist (e.g. `version = file: VERSION.txt`)326 self._referenced_files = set[str]()327 328 self.set_defaults = ConfigDiscovery(self)329 330 self._set_metadata_defaults(attrs)331 332 self.metadata.version = self._normalize_version(self.metadata.version)333 self._finalize_requires()334 335 def _validate_metadata(self):336 required = {"name"}337 provided = {338 key339 for key in vars(self.metadata)340 if getattr(self.metadata, key, None) is not None341 }342 missing = required - provided343 344 if missing:345 msg = f"Required package metadata is missing: {missing}"346 raise DistutilsSetupError(msg)347 348 def _set_metadata_defaults(self, attrs):349 """350 Fill-in missing metadata fields not supported by distutils.351 Some fields may have been set by other tools (e.g. pbr).352 Those fields (vars(self.metadata)) take precedence to353 supplied attrs.354 """355 for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():356 vars(self.metadata).setdefault(option, attrs.get(option, default()))357 358 @staticmethod359 def _normalize_version(version):360 from . import sic361 362 if isinstance(version, numbers.Number):363 # Some people apparently take "version number" too literally :)364 version = str(version)365 elif isinstance(version, sic) or version is None:366 return version367 368 normalized = str(Version(version))369 if version != normalized:370 InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")371 return normalized372 return version373 374 def _finalize_requires(self):375 """376 Set `metadata.python_requires` and fix environment markers377 in `install_requires` and `extras_require`.378 """379 if getattr(self, 'python_requires', None):380 self.metadata.python_requires = self.python_requires381 382 self._normalize_requires()383 self.metadata.install_requires = self.install_requires384 self.metadata.extras_require = self.extras_require385 386 if self.extras_require:387 for extra in self.extras_require.keys():388 # Setuptools allows a weird "<name>:<env markers> syntax for extras389 extra = extra.split(':')[0]390 if extra:391 self.metadata.provides_extras.setdefault(extra)392 393 def _normalize_requires(self):394 """Make sure requirement-related attributes exist and are normalized"""395 install_requires = getattr(self, "install_requires", None) or []396 extras_require = getattr(self, "extras_require", None) or {}397 398 # Preserve the "static"-ness of values parsed from config files399 list_ = _static.List if _static.is_static(install_requires) else list400 self.install_requires = list_(map(str, _reqs.parse(install_requires)))401 402 dict_ = _static.Dict if _static.is_static(extras_require) else dict403 self.extras_require = dict_(404 (k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items()405 )406 407 def _finalize_license_expression(self) -> None:408 """409 Normalize license and license_expression.410 >>> dist = Distribution({"license_expression": _static.Str("mit aNd gpl-3.0-OR-later")})411 >>> _static.is_static(dist.metadata.license_expression)412 True413 >>> dist._finalize_license_expression()414 >>> _static.is_static(dist.metadata.license_expression) # preserve "static-ness"415 True416 >>> print(dist.metadata.license_expression)417 MIT AND GPL-3.0-or-later418 """419 classifiers = self.metadata.get_classifiers()420 license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")]421 422 license_expr = self.metadata.license_expression423 if license_expr:424 str_ = _static.Str if _static.is_static(license_expr) else str425 normalized = str_(_canonicalize_license_expression(license_expr))426 if license_expr != normalized:427 InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'")428 self.metadata.license_expression = normalized429 if license_classifiers:430 raise InvalidConfigError(431 "License classifiers have been superseded by license expressions "432 "(see https://peps.python.org/pep-0639/). Please remove:\n\n"433 + "\n".join(license_classifiers),434 )435 elif license_classifiers:436 pypa_guides = "guides/writing-pyproject-toml/#license"437 SetuptoolsDeprecationWarning.emit(438 "License classifiers are deprecated.",439 "Please consider removing the following classifiers in favor of a "440 "SPDX license expression:\n\n" + "\n".join(license_classifiers),441 see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",442 # Warning introduced on 2025-02-17443 # TODO: Should we add a due date? It may affect old/unmaintained444 # packages in the ecosystem and cause problems...445 )446 447 def _finalize_license_files(self) -> None:448 """Compute names of all license files which should be included."""449 license_files: list[str] | None = self.metadata.license_files450 patterns = license_files or []451 452 license_file: str | None = self.metadata.license_file453 if license_file and license_file not in patterns:454 patterns.append(license_file)455 456 if license_files is None and license_file is None:457 # Default patterns match the ones wheel uses458 # See https://wheel.readthedocs.io/en/stable/user_guide.html459 # -> 'Including license files in the generated wheel file'460 patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']461 files = self._expand_patterns(patterns, enforce_match=False)462 else: # Patterns explicitly given by the user463 files = self._expand_patterns(patterns, enforce_match=True)464 465 self.metadata.license_files = list(unique_everseen(files))466 467 @classmethod468 def _expand_patterns(469 cls, patterns: list[str], enforce_match: bool = True470 ) -> Iterator[str]:471 """472 >>> getfixture('sample_project_cwd')473 >>> list(Distribution._expand_patterns(['LICENSE.txt']))474 ['LICENSE.txt']475 >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))476 ['pyproject.toml', 'LICENSE.txt']477 >>> list(Distribution._expand_patterns(['src/**/*.dat']))478 ['src/sample/package_data.dat']479 """480 return (481 path.replace(os.sep, "/")482 for pattern in patterns483 for path in sorted(cls._find_pattern(pattern, enforce_match))484 if not path.endswith('~') and os.path.isfile(path)485 )486 487 @staticmethod488 def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]:489 r"""490 >>> getfixture('sample_project_cwd')491 >>> Distribution._find_pattern("LICENSE.txt")492 ['LICENSE.txt']493 >>> Distribution._find_pattern("/LICENSE.MIT")494 Traceback (most recent call last):495 ...496 setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative...497 >>> Distribution._find_pattern("../LICENSE.MIT")498 Traceback (most recent call last):499 ...500 setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern '../LICENSE.MIT' cannot contain '..'...501 >>> Distribution._find_pattern("LICEN{CSE*")502 Traceback (most recent call last):503 ...504 setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters...505 """506 pypa_guides = "specifications/glob-patterns/"507 if ".." in pattern:508 SetuptoolsDeprecationWarning.emit(509 f"Pattern {pattern!r} cannot contain '..'",510 """511 Please ensure the files specified are contained by the root512 of the Python package (normally marked by `pyproject.toml`).513 """,514 see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",515 due_date=(2027, 2, 18), # Introduced in 2025-03-20516 # Replace with InvalidConfigError after deprecation517 )518 if pattern.startswith((os.sep, "/")) or ":\\" in pattern:519 raise InvalidConfigError(520 f"Pattern {pattern!r} should be relative and must not start with '/'"521 )522 if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None:523 SetuptoolsDeprecationWarning.emit(524 "Please provide a valid glob pattern.",525 "Pattern {pattern!r} contains invalid characters.",526 pattern=pattern,527 see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",528 due_date=(2027, 2, 18), # Introduced in 2025-02-20529 )530 531 found = glob(pattern, recursive=True)532 533 if enforce_match and not found:534 SetuptoolsDeprecationWarning.emit(535 "Cannot find any files for the given pattern.",536 "Pattern {pattern!r} did not match any files.",537 pattern=pattern,538 due_date=(2027, 2, 18), # Introduced in 2025-02-20539 # PEP 639 requires us to error, but as a transition period540 # we will only issue a warning to give people time to prepare.541 # After the transition, this should raise an InvalidConfigError.542 )543 return found544 545 # FIXME: 'Distribution._parse_config_files' is too complex (14)546 def _parse_config_files(self, filenames=None): # noqa: C901547 """548 Adapted from distutils.dist.Distribution.parse_config_files,549 this method provides the same functionality in subtly-improved550 ways.551 """552 from configparser import ConfigParser553 554 # Ignore install directory options if we have a venv555 ignore_options = (556 []557 if sys.prefix == sys.base_prefix558 else [559 'install-base',560 'install-platbase',561 'install-lib',562 'install-platlib',563 'install-purelib',564 'install-headers',565 'install-scripts',566 'install-data',567 'prefix',568 'exec-prefix',569 'home',570 'user',571 'root',572 ]573 )574 575 ignore_options = frozenset(ignore_options)576 577 if filenames is None:578 filenames = self.find_config_files()579 580 if DEBUG:581 self.announce("Distribution.parse_config_files():")582 583 parser = ConfigParser()584 parser.optionxform = str585 for filename in filenames:586 with open(filename, encoding='utf-8') as reader:587 if DEBUG:588 self.announce(" reading {filename}".format(**locals()))589 parser.read_file(reader)590 for section in parser.sections():591 options = parser.options(section)592 opt_dict = self.get_option_dict(section)593 594 for opt in options:595 if opt == '__name__' or opt in ignore_options:596 continue597 598 val = parser.get(section, opt)599 opt = self._enforce_underscore(opt, section)600 opt = self._enforce_option_lowercase(opt, section)601 opt_dict[opt] = (filename, val)602 603 # Make the ConfigParser forget everything (so we retain604 # the original filenames that options come from)605 parser.__init__()606 607 if 'global' not in self.command_options:608 return609 610 # If there was a "global" section in the config file, use it611 # to set Distribution options.612 613 for opt, (src, val) in self.command_options['global'].items():614 alias = self.negative_opt.get(opt)615 if alias:616 val = not strtobool(val)617 elif opt == 'verbose':618 val = strtobool(val)619 620 try:621 setattr(self, alias or opt, val)622 except ValueError as e:623 raise DistutilsOptionError(e) from e624 625 def _enforce_underscore(self, opt: str, section: str) -> str:626 if "-" not in opt or self._skip_setupcfg_normalization(section):627 return opt628 629 underscore_opt = opt.replace('-', '_')630 affected = f"(Affected: {self.metadata.name})." if self.metadata.name else ""631 SetuptoolsDeprecationWarning.emit(632 f"Invalid dash-separated key {opt!r} in {section!r} (setup.cfg), "633 f"please use the underscore name {underscore_opt!r} instead.",634 f"""635 Usage of dash-separated {opt!r} will not be supported in future636 versions. Please use the underscore name {underscore_opt!r} instead.637 {affected}638 639 Available configuration options are listed in:640 https://setuptools.pypa.io/en/latest/userguide/declarative_config.html641 """,642 see_url="https://github.com/pypa/setuptools/discussions/5011",643 due_date=(2026, 3, 3),644 # Warning initially introduced in 3 Mar 2021645 )646 return underscore_opt647 648 def _enforce_option_lowercase(self, opt: str, section: str) -> str:649 if opt.islower() or self._skip_setupcfg_normalization(section):650 return opt651 652 lowercase_opt = opt.lower()653 affected = f"(Affected: {self.metadata.name})." if self.metadata.name else ""654 SetuptoolsDeprecationWarning.emit(655 f"Invalid uppercase key {opt!r} in {section!r} (setup.cfg), "656 f"please use lowercase {lowercase_opt!r} instead.",657 f"""658 Usage of uppercase key {opt!r} in {section!r} will not be supported in659 future versions. Please use lowercase {lowercase_opt!r} instead.660 {affected}661 662 Available configuration options are listed in:663 https://setuptools.pypa.io/en/latest/userguide/declarative_config.html664 """,665 see_url="https://github.com/pypa/setuptools/discussions/5011",666 due_date=(2026, 3, 3),667 # Warning initially introduced in 6 Mar 2021668 )669 return lowercase_opt670 671 def _skip_setupcfg_normalization(self, section: str) -> bool:672 skip = (673 'options.extras_require',674 'options.data_files',675 'options.entry_points',676 'options.package_data',677 'options.exclude_package_data',678 )679 return section in skip or not self._is_setuptools_section(section)680 681 def _is_setuptools_section(self, section: str) -> bool:682 return (683 section == "metadata"684 or section.startswith("options")685 or section in _setuptools_commands()686 )687 688 # FIXME: 'Distribution._set_command_options' is too complex (14)689 def _set_command_options(self, command_obj, option_dict=None): # noqa: C901690 """691 Set the options for 'command_obj' from 'option_dict'. Basically692 this means copying elements of a dictionary ('option_dict') to693 attributes of an instance ('command').694 695 'command_obj' must be a Command instance. If 'option_dict' is not696 supplied, uses the standard option dictionary for this command697 (from 'self.command_options').698 699 (Adopted from distutils.dist.Distribution._set_command_options)700 """701 command_name = command_obj.get_command_name()702 if option_dict is None:703 option_dict = self.get_option_dict(command_name)704 705 if DEBUG:706 self.announce(f" setting options for '{command_name}' command:")707 for option, (source, value) in option_dict.items():708 if DEBUG:709 self.announce(f" {option} = {value} (from {source})")710 try:711 bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]712 except AttributeError:713 bool_opts = []714 try:715 neg_opt = command_obj.negative_opt716 except AttributeError:717 neg_opt = {}718 719 try:720 is_string = isinstance(value, str)721 if option in neg_opt and is_string:722 setattr(command_obj, neg_opt[option], not strtobool(value))723 elif option in bool_opts and is_string:724 setattr(command_obj, option, strtobool(value))725 elif hasattr(command_obj, option):726 setattr(command_obj, option, value)727 else:728 raise DistutilsOptionError(729 f"error in {source}: command '{command_name}' has no such option '{option}'"730 )731 except ValueError as e:732 raise DistutilsOptionError(e) from e733 734 def _get_project_config_files(self, filenames: Iterable[StrPath] | None):735 """Add default file and split between INI and TOML"""736 tomlfiles = []737 standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")738 if filenames is not None:739 parts = partition(lambda f: Path(f).suffix == ".toml", filenames)740 filenames = list(parts[0]) # 1st element => predicate is False741 tomlfiles = list(parts[1]) # 2nd element => predicate is True742 elif standard_project_metadata.exists():743 tomlfiles = [standard_project_metadata]744 return filenames, tomlfiles745 746 def parse_config_files(747 self,748 filenames: Iterable[StrPath] | None = None,749 ignore_option_errors: bool = False,750 ) -> None:751 """Parses configuration files from various levels752 and loads configuration.753 """754 inifiles, tomlfiles = self._get_project_config_files(filenames)755 756 self._parse_config_files(filenames=inifiles)757 758 setupcfg.parse_configuration(759 self, self.command_options, ignore_option_errors=ignore_option_errors760 )761 for filename in tomlfiles:762 pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)763 764 self._finalize_requires()765 self._finalize_license_expression()766 self._finalize_license_files()767 768 def fetch_build_eggs(self, requires: _StrOrIter) -> list[metadata.Distribution]:769 """Resolve pre-setup requirements"""770 from .installer import _fetch_build_eggs771 772 return _fetch_build_eggs(self, requires)773 774 def finalize_options(self) -> None:775 """776 Allow plugins to apply arbitrary operations to the777 distribution. Each hook may optionally define a 'order'778 to influence the order of execution. Smaller numbers779 go first and the default is 0.780 """781 group = 'setuptools.finalize_distribution_options'782 783 def by_order(hook):784 return getattr(hook, 'order', 0)785 786 defined = metadata.entry_points(group=group)787 filtered = itertools.filterfalse(self._removed, defined)788 loaded = map(lambda e: e.load(), filtered)789 for ep in sorted(loaded, key=by_order):790 ep(self)791 792 @staticmethod793 def _removed(ep):794 """795 When removing an entry point, if metadata is loaded796 from an older version of Setuptools, that removed797 entry point will attempt to be loaded and will fail.798 See #2765 for more details.799 """800 removed = {801 # removed 2021-09-05802 '2to3_doctests',803 }804 return ep.name in removed805 806 def _finalize_setup_keywords(self):807 for ep in metadata.entry_points(group='distutils.setup_keywords'):808 value = getattr(self, ep.name, None)809 if value is not None:810 ep.load()(self, ep.name, value)811 812 def get_egg_cache_dir(self) -> str:813 from . import windows_support814 815 egg_cache_dir = os.path.join(os.curdir, '.eggs')816 if not os.path.exists(egg_cache_dir):817 os.mkdir(egg_cache_dir)818 windows_support.hide_file(egg_cache_dir)819 readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')820 with open(readme_txt_filename, 'w', encoding="utf-8") as f:821 f.write(822 'This directory contains eggs that were downloaded '823 'by setuptools to build, test, and run plug-ins.\n\n'824 )825 f.write(826 'This directory caches those eggs to prevent '827 'repeated downloads.\n\n'828 )829 f.write('However, it is safe to delete this directory.\n\n')830 831 return egg_cache_dir832 833 def fetch_build_egg(self, req):834 """Fetch an egg needed for building"""835 from .installer import fetch_build_egg836 837 return fetch_build_egg(self, req)838 839 def get_command_class(self, command: str) -> type[distutils.cmd.Command]: # type: ignore[override] # Not doing complex overrides yet840 """Pluggable version of get_command_class()"""841 if command in self.cmdclass:842 return self.cmdclass[command]843 844 # Special case bdist_wheel so it's never loaded from "wheel"845 if command == 'bdist_wheel':846 from .command.bdist_wheel import bdist_wheel847 848 return bdist_wheel849 850 eps = metadata.entry_points(group='distutils.commands', name=command)851 for ep in eps:852 self.cmdclass[command] = cmdclass = ep.load()853 return cmdclass854 else:855 return _Distribution.get_command_class(self, command)856 857 def print_commands(self):858 for ep in metadata.entry_points(group='distutils.commands'):859 if ep.name not in self.cmdclass:860 cmdclass = ep.load()861 self.cmdclass[ep.name] = cmdclass862 return _Distribution.print_commands(self)863 864 def get_command_list(self):865 for ep in metadata.entry_points(group='distutils.commands'):866 if ep.name not in self.cmdclass:867 cmdclass = ep.load()868 self.cmdclass[ep.name] = cmdclass869 return _Distribution.get_command_list(self)870 871 def include(self, **attrs) -> None:872 """Add items to distribution that are named in keyword arguments873 874 For example, 'dist.include(py_modules=["x"])' would add 'x' to875 the distribution's 'py_modules' attribute, if it was not already876 there.877 878 Currently, this method only supports inclusion for attributes that are879 lists or tuples. If you need to add support for adding to other880 attributes in this or a subclass, you can add an '_include_X' method,881 where 'X' is the name of the attribute. The method will be called with882 the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})'883 will try to call 'dist._include_foo({"bar":"baz"})', which can then884 handle whatever special inclusion logic is needed.885 """886 for k, v in attrs.items():887 include = getattr(self, '_include_' + k, None)888 if include:889 include(v)890 else:891 self._include_misc(k, v)892 893 def exclude_package(self, package: str) -> None:894 """Remove packages, modules, and extensions in named package"""895 896 pfx = package + '.'897 if self.packages:898 self.packages = [899 p for p in self.packages if p != package and not p.startswith(pfx)900 ]901 902 if self.py_modules:903 self.py_modules = [904 p for p in self.py_modules if p != package and not p.startswith(pfx)905 ]906 907 if self.ext_modules:908 self.ext_modules = [909 p910 for p in self.ext_modules911 if p.name != package and not p.name.startswith(pfx)912 ]913 914 def has_contents_for(self, package: str) -> bool:915 """Return true if 'exclude_package(package)' would do something"""916 917 pfx = package + '.'918 919 for p in self.iter_distribution_names():920 if p == package or p.startswith(pfx):921 return True922 923 return False924 925 def _exclude_misc(self, name: str, value: _Sequence) -> None:926 """Handle 'exclude()' for list/tuple attrs without a special handler"""927 if not isinstance(value, _sequence):928 raise DistutilsSetupError(929 f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})"930 )931 try:932 old = getattr(self, name)933 except AttributeError as e:934 raise DistutilsSetupError(f"{name}: No such distribution setting") from e935 if old is not None and not isinstance(old, _sequence):936 raise DistutilsSetupError(937 name + ": this setting cannot be changed via include/exclude"938 )939 elif old:940 setattr(self, name, [item for item in old if item not in value])941 942 def _include_misc(self, name: str, value: _Sequence) -> None:943 """Handle 'include()' for list/tuple attrs without a special handler"""944 945 if not isinstance(value, _sequence):946 raise DistutilsSetupError(947 f"{name}: setting must be of type <{_sequence_type_repr}> (got {value!r})"948 )949 try:950 old = getattr(self, name)951 except AttributeError as e:952 raise DistutilsSetupError(f"{name}: No such distribution setting") from e953 if old is None:954 setattr(self, name, value)955 elif not isinstance(old, _sequence):956 raise DistutilsSetupError(957 name + ": this setting cannot be changed via include/exclude"958 )959 else:960 new = [item for item in value if item not in old]961 setattr(self, name, list(old) + new)962 963 def exclude(self, **attrs) -> None:964 """Remove items from distribution that are named in keyword arguments965 966 For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from967 the distribution's 'py_modules' attribute. Excluding packages uses968 the 'exclude_package()' method, so all of the package's contained969 packages, modules, and extensions are also excluded.970 971 Currently, this method only supports exclusion from attributes that are972 lists or tuples. If you need to add support for excluding from other973 attributes in this or a subclass, you can add an '_exclude_X' method,974 where 'X' is the name of the attribute. The method will be called with975 the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})'976 will try to call 'dist._exclude_foo({"bar":"baz"})', which can then977 handle whatever special exclusion logic is needed.978 """979 for k, v in attrs.items():980 exclude = getattr(self, '_exclude_' + k, None)981 if exclude:982 exclude(v)983 else:984 self._exclude_misc(k, v)985 986 def _exclude_packages(self, packages: _Sequence) -> None:987 if not isinstance(packages, _sequence):988 raise DistutilsSetupError(989 f"packages: setting must be of type <{_sequence_type_repr}> (got {packages!r})"990 )991 list(map(self.exclude_package, packages))992 993 def _parse_command_opts(self, parser, args):994 # Remove --with-X/--without-X options when processing command args995 self.global_options = self.__class__.global_options996 self.negative_opt = self.__class__.negative_opt997 998 # First, expand any aliases999 command = args[0]1000 aliases = self.get_option_dict('aliases')1001 while command in aliases:1002 _src, alias = aliases[command]1003 del aliases[command] # ensure each alias can expand only once!1004 import shlex1005 1006 args[:1] = shlex.split(alias, True)1007 command = args[0]1008 1009 nargs = _Distribution._parse_command_opts(self, parser, args)1010 1011 # Handle commands that want to consume all remaining arguments1012 cmd_class = self.get_command_class(command)1013 if getattr(cmd_class, 'command_consumes_arguments', None):1014 self.get_option_dict(command)['args'] = ("command line", nargs)1015 if nargs is not None:1016 return []1017 1018 return nargs1019 1020 def get_cmdline_options(self) -> dict[str, dict[str, str | None]]:1021 """Return a '{cmd: {opt:val}}' map of all command-line options1022 1023 Option names are all long, but do not include the leading '--', and1024 contain dashes rather than underscores. If the option doesn't take1025 an argument (e.g. '--quiet'), the 'val' is 'None'.1026 1027 Note that options provided by config files are intentionally excluded.1028 """1029 1030 d: dict[str, dict[str, str | None]] = {}1031 1032 for cmd, opts in self.command_options.items():1033 val: str | None1034 for opt, (src, val) in opts.items():1035 if src != "command line":1036 continue1037 1038 opt = opt.replace('_', '-')1039 1040 if val == 0:1041 cmdobj = self.get_command_obj(cmd)1042 neg_opt = self.negative_opt.copy()1043 neg_opt.update(getattr(cmdobj, 'negative_opt', {}))1044 for neg, pos in neg_opt.items():1045 if pos == opt:1046 opt = neg1047 val = None1048 break1049 else:1050 raise AssertionError("Shouldn't be able to get here")1051 1052 elif val == 1:1053 val = None1054 1055 d.setdefault(cmd, {})[opt] = val1056 1057 return d1058 1059 def iter_distribution_names(self) -> Iterator[str]:1060 """Yield all packages, modules, and extension names in distribution"""1061 1062 yield from self.packages or ()1063 1064 yield from self.py_modules or ()1065 1066 for ext in self.ext_modules or ():1067 if isinstance(ext, tuple):1068 name, _buildinfo = ext1069 else:1070 name = ext.name1071 name = name.removesuffix('module')1072 yield name1073 1074 def handle_display_options(self, option_order):1075 """If there were any non-global "display-only" options1076 (--help-commands or the metadata display options) on the command1077 line, display the requested info and return true; else return1078 false.1079 """1080 import sys1081 1082 if self.help_commands:1083 return _Distribution.handle_display_options(self, option_order)1084 1085 # Stdout may be StringIO (e.g. in tests)1086 if not isinstance(sys.stdout, io.TextIOWrapper):1087 return _Distribution.handle_display_options(self, option_order)1088 1089 # Don't wrap stdout if utf-8 is already the encoding. Provides1090 # workaround for #334.1091 if sys.stdout.encoding.lower() in ('utf-8', 'utf8'):1092 return _Distribution.handle_display_options(self, option_order)1093 1094 # Print metadata in UTF-8 no matter the platform1095 encoding = sys.stdout.encoding1096 sys.stdout.reconfigure(encoding='utf-8')1097 try:1098 return _Distribution.handle_display_options(self, option_order)1099 finally:1100 sys.stdout.reconfigure(encoding=encoding)1101 1102 def run_command(self, command) -> None:1103 self.set_defaults()1104 # Postpone defaults until all explicit configuration is considered1105 # (setup() args, config files, command line and plugins)1106 1107 super().run_command(command)1108 1109 1110@functools.cache1111def _setuptools_commands() -> set[str]:1112 try:1113 # Use older API for importlib.metadata compatibility1114 entry_points = metadata.distribution('setuptools').entry_points1115 eps: Iterable[str] = (ep.name for ep in entry_points)1116 except metadata.PackageNotFoundError:1117 # during bootstrapping, distribution doesn't exist1118 eps = []1119 return {*distutils.command.__all__, *eps}1120 1121 1122class DistDeprecationWarning(SetuptoolsDeprecationWarning):1123 """Class for warning about deprecations in dist in1124 setuptools. Not ignored by default, unlike DeprecationWarning."""1125 