codekingpro/portable-devtools
114k
1__all__ = ['Distribution']2 3 4import io5import itertools6import numbers7import os8import re9import sys10from contextlib import suppress11from glob import iglob12from pathlib import Path13from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Set, Tuple14 15import distutils.cmd16import distutils.command17import distutils.core18import distutils.dist19import distutils.log20from distutils.debug import DEBUG21from distutils.errors import DistutilsOptionError, DistutilsSetupError22from distutils.fancy_getopt import translate_longopt23from distutils.util import strtobool24 25from .extern.more_itertools import partition, unique_everseen26from .extern.ordered_set import OrderedSet27from .extern.packaging.markers import InvalidMarker, Marker28from .extern.packaging.specifiers import InvalidSpecifier, SpecifierSet29from .extern.packaging.version import Version30 31from . import _entry_points32from . import _normalization33from . import _reqs34from . import command as _ # noqa -- imported for side-effects35from ._importlib import metadata36from .config import setupcfg, pyprojecttoml37from .discovery import ConfigDiscovery38from .monkey import get_unpatched39from .warnings import InformationOnly, SetuptoolsDeprecationWarning40 41 42sequence = tuple, list43 44 45def check_importable(dist, attr, value):46 try:47 ep = metadata.EntryPoint(value=value, name=None, group=None)48 assert not ep.extras49 except (TypeError, ValueError, AttributeError, AssertionError) as e:50 raise DistutilsSetupError(51 "%r must be importable 'module:attrs' string (got %r)" % (attr, value)52 ) from e53 54 55def assert_string_list(dist, attr, value):56 """Verify that value is a string list"""57 try:58 # verify that value is a list or tuple to exclude unordered59 # or single-use iterables60 assert isinstance(value, (list, tuple))61 # verify that elements of value are strings62 assert ''.join(value) != value63 except (TypeError, ValueError, AttributeError, AssertionError) as e:64 raise DistutilsSetupError(65 "%r must be a list of strings (got %r)" % (attr, value)66 ) from e67 68 69def check_nsp(dist, attr, value):70 """Verify that namespace packages are valid"""71 ns_packages = value72 assert_string_list(dist, attr, ns_packages)73 for nsp in ns_packages:74 if not dist.has_contents_for(nsp):75 raise DistutilsSetupError(76 "Distribution contains no modules or packages for "77 + "namespace package %r" % nsp78 )79 parent, sep, child = nsp.rpartition('.')80 if parent and parent not in ns_packages:81 distutils.log.warn(82 "WARNING: %r is declared as a package namespace, but %r"83 " is not: please correct this in setup.py",84 nsp,85 parent,86 )87 SetuptoolsDeprecationWarning.emit(88 "The namespace_packages parameter is deprecated.",89 "Please replace its usage with implicit namespaces (PEP 420).",90 see_docs="references/keywords.html#keyword-namespace-packages",91 # TODO: define due_date, it may break old packages that are no longer92 # maintained (e.g. sphinxcontrib extensions) when installed from source.93 # Warning officially introduced in May 2022, however the deprecation94 # was mentioned much earlier in the docs (May 2020, see #2149).95 )96 97 98def check_extras(dist, attr, value):99 """Verify that extras_require mapping is valid"""100 try:101 list(itertools.starmap(_check_extra, value.items()))102 except (TypeError, ValueError, AttributeError) as e:103 raise DistutilsSetupError(104 "'extras_require' must be a dictionary whose values are "105 "strings or lists of strings containing valid project/version "106 "requirement specifiers."107 ) from e108 109 110def _check_extra(extra, reqs):111 name, sep, marker = extra.partition(':')112 try:113 _check_marker(marker)114 except InvalidMarker:115 msg = f"Invalid environment marker: {marker} ({extra!r})"116 raise DistutilsSetupError(msg) from None117 list(_reqs.parse(reqs))118 119 120def _check_marker(marker):121 if not marker:122 return123 m = Marker(marker)124 m.evaluate()125 126 127def assert_bool(dist, attr, value):128 """Verify that value is True, False, 0, or 1"""129 if bool(value) != value:130 tmpl = "{attr!r} must be a boolean value (got {value!r})"131 raise DistutilsSetupError(tmpl.format(attr=attr, value=value))132 133 134def invalid_unless_false(dist, attr, value):135 if not value:136 DistDeprecationWarning.emit(f"{attr} is ignored.")137 # TODO: should there be a `due_date` here?138 return139 raise DistutilsSetupError(f"{attr} is invalid.")140 141 142def check_requirements(dist, attr, value):143 """Verify that install_requires is a valid requirements list"""144 try:145 list(_reqs.parse(value))146 if isinstance(value, (dict, set)):147 raise TypeError("Unordered types are not allowed")148 except (TypeError, ValueError) as error:149 tmpl = (150 "{attr!r} must be a string or list of strings "151 "containing valid project/version requirement specifiers; {error}"152 )153 raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error154 155 156def check_specifier(dist, attr, value):157 """Verify that value is a valid version specifier"""158 try:159 SpecifierSet(value)160 except (InvalidSpecifier, AttributeError) as error:161 tmpl = (162 "{attr!r} must be a string " "containing valid version specifiers; {error}"163 )164 raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error165 166 167def check_entry_points(dist, attr, value):168 """Verify that entry_points map is parseable"""169 try:170 _entry_points.load(value)171 except Exception as e:172 raise DistutilsSetupError(e) from e173 174 175def check_test_suite(dist, attr, value):176 if not isinstance(value, str):177 raise DistutilsSetupError("test_suite must be a string")178 179 180def check_package_data(dist, attr, value):181 """Verify that value is a dictionary of package names to glob lists"""182 if not isinstance(value, dict):183 raise DistutilsSetupError(184 "{!r} must be a dictionary mapping package names to lists of "185 "string wildcard patterns".format(attr)186 )187 for k, v in value.items():188 if not isinstance(k, str):189 raise DistutilsSetupError(190 "keys of {!r} dict must be strings (got {!r})".format(attr, k)191 )192 assert_string_list(dist, 'values of {!r} dict'.format(attr), v)193 194 195def check_packages(dist, attr, value):196 for pkgname in value:197 if not re.match(r'\w+(\.\w+)*', pkgname):198 distutils.log.warn(199 "WARNING: %r not a valid package name; please use only "200 ".-separated package names in setup.py",201 pkgname,202 )203 204 205if TYPE_CHECKING:206 # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962207 _Distribution = distutils.core.Distribution208else:209 _Distribution = get_unpatched(distutils.core.Distribution)210 211 212class Distribution(_Distribution):213 """Distribution with support for tests and package data214 215 This is an enhanced version of 'distutils.dist.Distribution' that216 effectively adds the following new optional keyword arguments to 'setup()':217 218 'install_requires' -- a string or sequence of strings specifying project219 versions that the distribution requires when installed, in the format220 used by 'pkg_resources.require()'. They will be installed221 automatically when the package is installed. If you wish to use222 packages that are not available in PyPI, or want to give your users an223 alternate download location, you can add a 'find_links' option to the224 '[easy_install]' section of your project's 'setup.cfg' file, and then225 setuptools will scan the listed web pages for links that satisfy the226 requirements.227 228 'extras_require' -- a dictionary mapping names of optional "extras" to the229 additional requirement(s) that using those extras incurs. For example,230 this::231 232 extras_require = dict(reST = ["docutils>=0.3", "reSTedit"])233 234 indicates that the distribution can optionally provide an extra235 capability called "reST", but it can only be used if docutils and236 reSTedit are installed. If the user installs your package using237 EasyInstall and requests one of your extras, the corresponding238 additional requirements will be installed if needed.239 240 'test_suite' -- the name of a test suite to run for the 'test' command.241 If the user runs 'python setup.py test', the package will be installed,242 and the named test suite will be run. The format is the same as243 would be used on a 'unittest.py' command line. That is, it is the244 dotted name of an object to import and call to generate a test suite.245 246 'package_data' -- a dictionary mapping package names to lists of filenames247 or globs to use to find data files contained in the named packages.248 If the dictionary has filenames or globs listed under '""' (the empty249 string), those names will be searched for in every package, in addition250 to any names for the specific package. Data files found using these251 names/globs will be installed along with the package, in the same252 location as the package. Note that globs are allowed to reference253 the contents of non-package subdirectories, as long as you use '/' as254 a path separator. (Globs are automatically converted to255 platform-specific paths at runtime.)256 257 In addition to these new keywords, this class also has several new methods258 for manipulating the distribution's contents. For example, the 'include()'259 and 'exclude()' methods can be thought of as in-place add and subtract260 commands that add or remove packages, modules, extensions, and so on from261 the distribution.262 """263 264 _DISTUTILS_UNSUPPORTED_METADATA = {265 'long_description_content_type': lambda: None,266 'project_urls': dict,267 'provides_extras': OrderedSet,268 'license_file': lambda: None,269 'license_files': lambda: None,270 'install_requires': list,271 'extras_require': dict,272 }273 274 _patched_dist = None275 276 def patch_missing_pkg_info(self, attrs):277 # Fake up a replacement for the data that would normally come from278 # PKG-INFO, but which might not yet be built if this is a fresh279 # checkout.280 #281 if not attrs or 'name' not in attrs or 'version' not in attrs:282 return283 name = _normalization.safe_name(str(attrs['name'])).lower()284 with suppress(metadata.PackageNotFoundError):285 dist = metadata.distribution(name)286 if dist is not None and not dist.read_text('PKG-INFO'):287 dist._version = _normalization.safe_version(str(attrs['version']))288 self._patched_dist = dist289 290 def __init__(self, attrs: Optional[MutableMapping] = None) -> None:291 have_package_data = hasattr(self, "package_data")292 if not have_package_data:293 self.package_data: Dict[str, List[str]] = {}294 attrs = attrs or {}295 self.dist_files: List[Tuple[str, str, str]] = []296 # Filter-out setuptools' specific options.297 self.src_root = attrs.pop("src_root", None)298 self.patch_missing_pkg_info(attrs)299 self.dependency_links = attrs.pop('dependency_links', [])300 self.setup_requires = attrs.pop('setup_requires', [])301 for ep in metadata.entry_points(group='distutils.setup_keywords'):302 vars(self).setdefault(ep.name, None)303 304 metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA)305 metadata_only -= {"install_requires", "extras_require"}306 dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only}307 _Distribution.__init__(self, dist_attrs)308 309 # Private API (setuptools-use only, not restricted to Distribution)310 # Stores files that are referenced by the configuration and need to be in the311 # sdist (e.g. `version = file: VERSION.txt`)312 self._referenced_files: Set[str] = set()313 314 self.set_defaults = ConfigDiscovery(self)315 316 self._set_metadata_defaults(attrs)317 318 self.metadata.version = self._normalize_version(self.metadata.version)319 self._finalize_requires()320 321 def _validate_metadata(self):322 required = {"name"}323 provided = {324 key325 for key in vars(self.metadata)326 if getattr(self.metadata, key, None) is not None327 }328 missing = required - provided329 330 if missing:331 msg = f"Required package metadata is missing: {missing}"332 raise DistutilsSetupError(msg)333 334 def _set_metadata_defaults(self, attrs):335 """336 Fill-in missing metadata fields not supported by distutils.337 Some fields may have been set by other tools (e.g. pbr).338 Those fields (vars(self.metadata)) take precedence to339 supplied attrs.340 """341 for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():342 vars(self.metadata).setdefault(option, attrs.get(option, default()))343 344 @staticmethod345 def _normalize_version(version):346 from . import sic347 348 if isinstance(version, numbers.Number):349 # Some people apparently take "version number" too literally :)350 version = str(version)351 elif isinstance(version, sic) or version is None:352 return version353 354 normalized = str(Version(version))355 if version != normalized:356 InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")357 return normalized358 return version359 360 def _finalize_requires(self):361 """362 Set `metadata.python_requires` and fix environment markers363 in `install_requires` and `extras_require`.364 """365 if getattr(self, 'python_requires', None):366 self.metadata.python_requires = self.python_requires367 368 self._normalize_requires()369 self.metadata.install_requires = self.install_requires370 self.metadata.extras_require = self.extras_require371 372 if self.extras_require:373 for extra in self.extras_require.keys():374 # Setuptools allows a weird "<name>:<env markers> syntax for extras375 extra = extra.split(':')[0]376 if extra:377 self.metadata.provides_extras.add(extra)378 379 def _normalize_requires(self):380 """Make sure requirement-related attributes exist and are normalized"""381 install_requires = getattr(self, "install_requires", None) or []382 extras_require = getattr(self, "extras_require", None) or {}383 self.install_requires = list(map(str, _reqs.parse(install_requires)))384 self.extras_require = {385 k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items()386 }387 388 def _finalize_license_files(self) -> None:389 """Compute names of all license files which should be included."""390 license_files: Optional[List[str]] = self.metadata.license_files391 patterns: List[str] = license_files if license_files else []392 393 license_file: Optional[str] = self.metadata.license_file394 if license_file and license_file not in patterns:395 patterns.append(license_file)396 397 if license_files is None and license_file is None:398 # Default patterns match the ones wheel uses399 # See https://wheel.readthedocs.io/en/stable/user_guide.html400 # -> 'Including license files in the generated wheel file'401 patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']402 403 self.metadata.license_files = list(404 unique_everseen(self._expand_patterns(patterns))405 )406 407 @staticmethod408 def _expand_patterns(patterns):409 """410 >>> list(Distribution._expand_patterns(['LICENSE']))411 ['LICENSE']412 >>> list(Distribution._expand_patterns(['setup.cfg', 'LIC*']))413 ['setup.cfg', 'LICENSE']414 """415 return (416 path417 for pattern in patterns418 for path in sorted(iglob(pattern))419 if not path.endswith('~') and os.path.isfile(path)420 )421 422 # FIXME: 'Distribution._parse_config_files' is too complex (14)423 def _parse_config_files(self, filenames=None): # noqa: C901424 """425 Adapted from distutils.dist.Distribution.parse_config_files,426 this method provides the same functionality in subtly-improved427 ways.428 """429 from configparser import ConfigParser430 431 # Ignore install directory options if we have a venv432 ignore_options = (433 []434 if sys.prefix == sys.base_prefix435 else [436 'install-base',437 'install-platbase',438 'install-lib',439 'install-platlib',440 'install-purelib',441 'install-headers',442 'install-scripts',443 'install-data',444 'prefix',445 'exec-prefix',446 'home',447 'user',448 'root',449 ]450 )451 452 ignore_options = frozenset(ignore_options)453 454 if filenames is None:455 filenames = self.find_config_files()456 457 if DEBUG:458 self.announce("Distribution.parse_config_files():")459 460 parser = ConfigParser()461 parser.optionxform = str462 for filename in filenames:463 with open(filename, encoding='utf-8') as reader:464 if DEBUG:465 self.announce(" reading {filename}".format(**locals()))466 parser.read_file(reader)467 for section in parser.sections():468 options = parser.options(section)469 opt_dict = self.get_option_dict(section)470 471 for opt in options:472 if opt == '__name__' or opt in ignore_options:473 continue474 475 val = parser.get(section, opt)476 opt = self.warn_dash_deprecation(opt, section)477 opt = self.make_option_lowercase(opt, section)478 opt_dict[opt] = (filename, val)479 480 # Make the ConfigParser forget everything (so we retain481 # the original filenames that options come from)482 parser.__init__()483 484 if 'global' not in self.command_options:485 return486 487 # If there was a "global" section in the config file, use it488 # to set Distribution options.489 490 for opt, (src, val) in self.command_options['global'].items():491 alias = self.negative_opt.get(opt)492 if alias:493 val = not strtobool(val)494 elif opt in ('verbose', 'dry_run'): # ugh!495 val = strtobool(val)496 497 try:498 setattr(self, alias or opt, val)499 except ValueError as e:500 raise DistutilsOptionError(e) from e501 502 def warn_dash_deprecation(self, opt, section):503 if section in (504 'options.extras_require',505 'options.data_files',506 ):507 return opt508 509 underscore_opt = opt.replace('-', '_')510 commands = list(511 itertools.chain(512 distutils.command.__all__,513 self._setuptools_commands(),514 )515 )516 if (517 not section.startswith('options')518 and section != 'metadata'519 and section not in commands520 ):521 return underscore_opt522 523 if '-' in opt:524 SetuptoolsDeprecationWarning.emit(525 "Invalid dash-separated options",526 f"""527 Usage of dash-separated {opt!r} will not be supported in future528 versions. Please use the underscore name {underscore_opt!r} instead.529 """,530 see_docs="userguide/declarative_config.html",531 due_date=(2024, 9, 26),532 # Warning initially introduced in 3 Mar 2021533 )534 return underscore_opt535 536 def _setuptools_commands(self):537 try:538 return metadata.distribution('setuptools').entry_points.names539 except metadata.PackageNotFoundError:540 # during bootstrapping, distribution doesn't exist541 return []542 543 def make_option_lowercase(self, opt, section):544 if section != 'metadata' or opt.islower():545 return opt546 547 lowercase_opt = opt.lower()548 SetuptoolsDeprecationWarning.emit(549 "Invalid uppercase configuration",550 f"""551 Usage of uppercase key {opt!r} in {section!r} will not be supported in552 future versions. Please use lowercase {lowercase_opt!r} instead.553 """,554 see_docs="userguide/declarative_config.html",555 due_date=(2024, 9, 26),556 # Warning initially introduced in 6 Mar 2021557 )558 return lowercase_opt559 560 # FIXME: 'Distribution._set_command_options' is too complex (14)561 def _set_command_options(self, command_obj, option_dict=None): # noqa: C901562 """563 Set the options for 'command_obj' from 'option_dict'. Basically564 this means copying elements of a dictionary ('option_dict') to565 attributes of an instance ('command').566 567 'command_obj' must be a Command instance. If 'option_dict' is not568 supplied, uses the standard option dictionary for this command569 (from 'self.command_options').570 571 (Adopted from distutils.dist.Distribution._set_command_options)572 """573 command_name = command_obj.get_command_name()574 if option_dict is None:575 option_dict = self.get_option_dict(command_name)576 577 if DEBUG:578 self.announce(" setting options for '%s' command:" % command_name)579 for option, (source, value) in option_dict.items():580 if DEBUG:581 self.announce(" %s = %s (from %s)" % (option, value, source))582 try:583 bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]584 except AttributeError:585 bool_opts = []586 try:587 neg_opt = command_obj.negative_opt588 except AttributeError:589 neg_opt = {}590 591 try:592 is_string = isinstance(value, str)593 if option in neg_opt and is_string:594 setattr(command_obj, neg_opt[option], not strtobool(value))595 elif option in bool_opts and is_string:596 setattr(command_obj, option, strtobool(value))597 elif hasattr(command_obj, option):598 setattr(command_obj, option, value)599 else:600 raise DistutilsOptionError(601 "error in %s: command '%s' has no such option '%s'"602 % (source, command_name, option)603 )604 except ValueError as e:605 raise DistutilsOptionError(e) from e606 607 def _get_project_config_files(self, filenames):608 """Add default file and split between INI and TOML"""609 tomlfiles = []610 standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")611 if filenames is not None:612 parts = partition(lambda f: Path(f).suffix == ".toml", filenames)613 filenames = list(parts[0]) # 1st element => predicate is False614 tomlfiles = list(parts[1]) # 2nd element => predicate is True615 elif standard_project_metadata.exists():616 tomlfiles = [standard_project_metadata]617 return filenames, tomlfiles618 619 def parse_config_files(self, filenames=None, ignore_option_errors=False):620 """Parses configuration files from various levels621 and loads configuration.622 """623 inifiles, tomlfiles = self._get_project_config_files(filenames)624 625 self._parse_config_files(filenames=inifiles)626 627 setupcfg.parse_configuration(628 self, self.command_options, ignore_option_errors=ignore_option_errors629 )630 for filename in tomlfiles:631 pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)632 633 self._finalize_requires()634 self._finalize_license_files()635 636 def fetch_build_eggs(self, requires):637 """Resolve pre-setup requirements"""638 from .installer import _fetch_build_eggs639 640 return _fetch_build_eggs(self, requires)641 642 def finalize_options(self):643 """644 Allow plugins to apply arbitrary operations to the645 distribution. Each hook may optionally define a 'order'646 to influence the order of execution. Smaller numbers647 go first and the default is 0.648 """649 group = 'setuptools.finalize_distribution_options'650 651 def by_order(hook):652 return getattr(hook, 'order', 0)653 654 defined = metadata.entry_points(group=group)655 filtered = itertools.filterfalse(self._removed, defined)656 loaded = map(lambda e: e.load(), filtered)657 for ep in sorted(loaded, key=by_order):658 ep(self)659 660 @staticmethod661 def _removed(ep):662 """663 When removing an entry point, if metadata is loaded664 from an older version of Setuptools, that removed665 entry point will attempt to be loaded and will fail.666 See #2765 for more details.667 """668 removed = {669 # removed 2021-09-05670 '2to3_doctests',671 }672 return ep.name in removed673 674 def _finalize_setup_keywords(self):675 for ep in metadata.entry_points(group='distutils.setup_keywords'):676 value = getattr(self, ep.name, None)677 if value is not None:678 ep.load()(self, ep.name, value)679 680 def get_egg_cache_dir(self):681 from . import windows_support682 683 egg_cache_dir = os.path.join(os.curdir, '.eggs')684 if not os.path.exists(egg_cache_dir):685 os.mkdir(egg_cache_dir)686 windows_support.hide_file(egg_cache_dir)687 readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')688 with open(readme_txt_filename, 'w') as f:689 f.write(690 'This directory contains eggs that were downloaded '691 'by setuptools to build, test, and run plug-ins.\n\n'692 )693 f.write(694 'This directory caches those eggs to prevent '695 'repeated downloads.\n\n'696 )697 f.write('However, it is safe to delete this directory.\n\n')698 699 return egg_cache_dir700 701 def fetch_build_egg(self, req):702 """Fetch an egg needed for building"""703 from .installer import fetch_build_egg704 705 return fetch_build_egg(self, req)706 707 def get_command_class(self, command):708 """Pluggable version of get_command_class()"""709 if command in self.cmdclass:710 return self.cmdclass[command]711 712 eps = metadata.entry_points(group='distutils.commands', name=command)713 for ep in eps:714 self.cmdclass[command] = cmdclass = ep.load()715 return cmdclass716 else:717 return _Distribution.get_command_class(self, command)718 719 def print_commands(self):720 for ep in metadata.entry_points(group='distutils.commands'):721 if ep.name not in self.cmdclass:722 cmdclass = ep.load()723 self.cmdclass[ep.name] = cmdclass724 return _Distribution.print_commands(self)725 726 def get_command_list(self):727 for ep in metadata.entry_points(group='distutils.commands'):728 if ep.name not in self.cmdclass:729 cmdclass = ep.load()730 self.cmdclass[ep.name] = cmdclass731 return _Distribution.get_command_list(self)732 733 def include(self, **attrs):734 """Add items to distribution that are named in keyword arguments735 736 For example, 'dist.include(py_modules=["x"])' would add 'x' to737 the distribution's 'py_modules' attribute, if it was not already738 there.739 740 Currently, this method only supports inclusion for attributes that are741 lists or tuples. If you need to add support for adding to other742 attributes in this or a subclass, you can add an '_include_X' method,743 where 'X' is the name of the attribute. The method will be called with744 the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})'745 will try to call 'dist._include_foo({"bar":"baz"})', which can then746 handle whatever special inclusion logic is needed.747 """748 for k, v in attrs.items():749 include = getattr(self, '_include_' + k, None)750 if include:751 include(v)752 else:753 self._include_misc(k, v)754 755 def exclude_package(self, package):756 """Remove packages, modules, and extensions in named package"""757 758 pfx = package + '.'759 if self.packages:760 self.packages = [761 p for p in self.packages if p != package and not p.startswith(pfx)762 ]763 764 if self.py_modules:765 self.py_modules = [766 p for p in self.py_modules if p != package and not p.startswith(pfx)767 ]768 769 if self.ext_modules:770 self.ext_modules = [771 p772 for p in self.ext_modules773 if p.name != package and not p.name.startswith(pfx)774 ]775 776 def has_contents_for(self, package):777 """Return true if 'exclude_package(package)' would do something"""778 779 pfx = package + '.'780 781 for p in self.iter_distribution_names():782 if p == package or p.startswith(pfx):783 return True784 785 return False786 787 def _exclude_misc(self, name, value):788 """Handle 'exclude()' for list/tuple attrs without a special handler"""789 if not isinstance(value, sequence):790 raise DistutilsSetupError(791 "%s: setting must be a list or tuple (%r)" % (name, value)792 )793 try:794 old = getattr(self, name)795 except AttributeError as e:796 raise DistutilsSetupError("%s: No such distribution setting" % name) from e797 if old is not None and not isinstance(old, sequence):798 raise DistutilsSetupError(799 name + ": this setting cannot be changed via include/exclude"800 )801 elif old:802 setattr(self, name, [item for item in old if item not in value])803 804 def _include_misc(self, name, value):805 """Handle 'include()' for list/tuple attrs without a special handler"""806 807 if not isinstance(value, sequence):808 raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value))809 try:810 old = getattr(self, name)811 except AttributeError as e:812 raise DistutilsSetupError("%s: No such distribution setting" % name) from e813 if old is None:814 setattr(self, name, value)815 elif not isinstance(old, sequence):816 raise DistutilsSetupError(817 name + ": this setting cannot be changed via include/exclude"818 )819 else:820 new = [item for item in value if item not in old]821 setattr(self, name, old + new)822 823 def exclude(self, **attrs):824 """Remove items from distribution that are named in keyword arguments825 826 For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from827 the distribution's 'py_modules' attribute. Excluding packages uses828 the 'exclude_package()' method, so all of the package's contained829 packages, modules, and extensions are also excluded.830 831 Currently, this method only supports exclusion from attributes that are832 lists or tuples. If you need to add support for excluding from other833 attributes in this or a subclass, you can add an '_exclude_X' method,834 where 'X' is the name of the attribute. The method will be called with835 the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})'836 will try to call 'dist._exclude_foo({"bar":"baz"})', which can then837 handle whatever special exclusion logic is needed.838 """839 for k, v in attrs.items():840 exclude = getattr(self, '_exclude_' + k, None)841 if exclude:842 exclude(v)843 else:844 self._exclude_misc(k, v)845 846 def _exclude_packages(self, packages):847 if not isinstance(packages, sequence):848 raise DistutilsSetupError(849 "packages: setting must be a list or tuple (%r)" % (packages,)850 )851 list(map(self.exclude_package, packages))852 853 def _parse_command_opts(self, parser, args):854 # Remove --with-X/--without-X options when processing command args855 self.global_options = self.__class__.global_options856 self.negative_opt = self.__class__.negative_opt857 858 # First, expand any aliases859 command = args[0]860 aliases = self.get_option_dict('aliases')861 while command in aliases:862 src, alias = aliases[command]863 del aliases[command] # ensure each alias can expand only once!864 import shlex865 866 args[:1] = shlex.split(alias, True)867 command = args[0]868 869 nargs = _Distribution._parse_command_opts(self, parser, args)870 871 # Handle commands that want to consume all remaining arguments872 cmd_class = self.get_command_class(command)873 if getattr(cmd_class, 'command_consumes_arguments', None):874 self.get_option_dict(command)['args'] = ("command line", nargs)875 if nargs is not None:876 return []877 878 return nargs879 880 def get_cmdline_options(self):881 """Return a '{cmd: {opt:val}}' map of all command-line options882 883 Option names are all long, but do not include the leading '--', and884 contain dashes rather than underscores. If the option doesn't take885 an argument (e.g. '--quiet'), the 'val' is 'None'.886 887 Note that options provided by config files are intentionally excluded.888 """889 890 d = {}891 892 for cmd, opts in self.command_options.items():893 for opt, (src, val) in opts.items():894 if src != "command line":895 continue896 897 opt = opt.replace('_', '-')898 899 if val == 0:900 cmdobj = self.get_command_obj(cmd)901 neg_opt = self.negative_opt.copy()902 neg_opt.update(getattr(cmdobj, 'negative_opt', {}))903 for neg, pos in neg_opt.items():904 if pos == opt:905 opt = neg906 val = None907 break908 else:909 raise AssertionError("Shouldn't be able to get here")910 911 elif val == 1:912 val = None913 914 d.setdefault(cmd, {})[opt] = val915 916 return d917 918 def iter_distribution_names(self):919 """Yield all packages, modules, and extension names in distribution"""920 921 yield from self.packages or ()922 923 yield from self.py_modules or ()924 925 for ext in self.ext_modules or ():926 if isinstance(ext, tuple):927 name, buildinfo = ext928 else:929 name = ext.name930 if name.endswith('module'):931 name = name[:-6]932 yield name933 934 def handle_display_options(self, option_order):935 """If there were any non-global "display-only" options936 (--help-commands or the metadata display options) on the command937 line, display the requested info and return true; else return938 false.939 """940 import sys941 942 if self.help_commands:943 return _Distribution.handle_display_options(self, option_order)944 945 # Stdout may be StringIO (e.g. in tests)946 if not isinstance(sys.stdout, io.TextIOWrapper):947 return _Distribution.handle_display_options(self, option_order)948 949 # Don't wrap stdout if utf-8 is already the encoding. Provides950 # workaround for #334.951 if sys.stdout.encoding.lower() in ('utf-8', 'utf8'):952 return _Distribution.handle_display_options(self, option_order)953 954 # Print metadata in UTF-8 no matter the platform955 encoding = sys.stdout.encoding956 sys.stdout.reconfigure(encoding='utf-8')957 try:958 return _Distribution.handle_display_options(self, option_order)959 finally:960 sys.stdout.reconfigure(encoding=encoding)961 962 def run_command(self, command):963 self.set_defaults()964 # Postpone defaults until all explicit configuration is considered965 # (setup() args, config files, command line and plugins)966 967 super().run_command(command)968 969 970class DistDeprecationWarning(SetuptoolsDeprecationWarning):971 """Class for warning about deprecations in dist in972 setuptools. Not ignored by default, unlike DeprecationWarning."""973 