1# -*- coding: utf-8 -*- 2__all__ = ['Distribution'] 3 4import io 5import sys 6import re 7import os 8import warnings 9import numbers 10import distutils.log 11import distutils.core 12import distutils.cmd 13import distutils.dist 14import distutils.command 15from distutils.util import strtobool 16from distutils.debug import DEBUG 17from distutils.fancy_getopt import translate_longopt 18from glob import iglob 19import itertools 20import textwrap 21from typing import List, Optional, TYPE_CHECKING 22from pathlib import Path 23 24from collections import defaultdict 25from email import message_from_file 26 27from distutils.errors import DistutilsOptionError, DistutilsSetupError 28from distutils.util import rfc822_escape 29 30from setuptools.extern import packaging 31from setuptools.extern import ordered_set 32from setuptools.extern.more_itertools import unique_everseen, partition 33from setuptools.extern import nspektr 34 35from ._importlib import metadata 36 37from . import SetuptoolsDeprecationWarning 38 39import setuptools 40import setuptools.command 41from setuptools import windows_support 42from setuptools.monkey import get_unpatched 43from setuptools.config import setupcfg, pyprojecttoml 44from setuptools.discovery import ConfigDiscovery 45 46import pkg_resources 47from setuptools.extern.packaging import version 48from . import _reqs 49from . import _entry_points 50 51if TYPE_CHECKING: 52 from email.message import Message 53 54__import__('setuptools.extern.packaging.specifiers') 55__import__('setuptools.extern.packaging.version') 56 57 58def _get_unpatched(cls): 59 warnings.warn("Do not call this function", DistDeprecationWarning) 60 return get_unpatched(cls) 61 62 63def get_metadata_version(self): 64 mv = getattr(self, 'metadata_version', None) 65 if mv is None: 66 mv = version.Version('2.1') 67 self.metadata_version = mv 68 return mv 69 70 71def rfc822_unescape(content: str) -> str: 72 """Reverse RFC-822 escaping by removing leading whitespaces from content.""" 73 lines = content.splitlines() 74 if len(lines) == 1: 75 return lines[0].lstrip() 76 return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:])))) 77 78 79def _read_field_from_msg(msg: "Message", field: str) -> Optional[str]: 80 """Read Message header field.""" 81 value = msg[field] 82 if value == 'UNKNOWN': 83 return None 84 return value 85 86 87def _read_field_unescaped_from_msg(msg: "Message", field: str) -> Optional[str]: 88 """Read Message header field and apply rfc822_unescape.""" 89 value = _read_field_from_msg(msg, field) 90 if value is None: 91 return value 92 return rfc822_unescape(value) 93 94 95def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]: 96 """Read Message header field and return all results as list.""" 97 values = msg.get_all(field, None) 98 if values == []: 99 return None 100 return values 101 102 103def _read_payload_from_msg(msg: "Message") -> Optional[str]: 104 value = msg.get_payload().strip() 105 if value == 'UNKNOWN': 106 return None 107 return value 108 109 110def read_pkg_file(self, file): 111 """Reads the metadata values from a file object.""" 112 msg = message_from_file(file) 113 114 self.metadata_version = version.Version(msg['metadata-version']) 115 self.name = _read_field_from_msg(msg, 'name') 116 self.version = _read_field_from_msg(msg, 'version') 117 self.description = _read_field_from_msg(msg, 'summary') 118 # we are filling author only. 119 self.author = _read_field_from_msg(msg, 'author') 120 self.maintainer = None 121 self.author_email = _read_field_from_msg(msg, 'author-email') 122 self.maintainer_email = None 123 self.url = _read_field_from_msg(msg, 'home-page') 124 self.download_url = _read_field_from_msg(msg, 'download-url') 125 self.license = _read_field_unescaped_from_msg(msg, 'license') 126 127 self.long_description = _read_field_unescaped_from_msg(msg, 'description') 128 if ( 129 self.long_description is None and 130 self.metadata_version >= version.Version('2.1') 131 ): 132 self.long_description = _read_payload_from_msg(msg) 133 self.description = _read_field_from_msg(msg, 'summary') 134 135 if 'keywords' in msg: 136 self.keywords = _read_field_from_msg(msg, 'keywords').split(',') 137 138 self.platforms = _read_list_from_msg(msg, 'platform') 139 self.classifiers = _read_list_from_msg(msg, 'classifier') 140 141 # PEP 314 - these fields only exist in 1.1 142 if self.metadata_version == version.Version('1.1'): 143 self.requires = _read_list_from_msg(msg, 'requires') 144 self.provides = _read_list_from_msg(msg, 'provides') 145 self.obsoletes = _read_list_from_msg(msg, 'obsoletes') 146 else: 147 self.requires = None 148 self.provides = None 149 self.obsoletes = None 150 151 self.license_files = _read_list_from_msg(msg, 'license-file') 152 153 154def single_line(val): 155 """ 156 Quick and dirty validation for Summary pypa/setuptools#1390. 157 """ 158 if '\n' in val: 159 # TODO: Replace with `raise ValueError("newlines not allowed")` 160 # after reviewing #2893. 161 warnings.warn("newlines not allowed and will break in the future") 162 val = val.strip().split('\n')[0] 163 return val 164 165 166# Based on Python 3.5 version 167def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME 168 """Write the PKG-INFO format data to a file object.""" 169 version = self.get_metadata_version() 170 171 def write_field(key, value): 172 file.write("%s: %s\n" % (key, value)) 173 174 write_field('Metadata-Version', str(version)) 175 write_field('Name', self.get_name()) 176 write_field('Version', self.get_version()) 177 write_field('Summary', single_line(self.get_description())) 178 179 optional_fields = ( 180 ('Home-page', 'url'), 181 ('Download-URL', 'download_url'), 182 ('Author', 'author'), 183 ('Author-email', 'author_email'), 184 ('Maintainer', 'maintainer'), 185 ('Maintainer-email', 'maintainer_email'), 186 ) 187 188 for field, attr in optional_fields: 189 attr_val = getattr(self, attr, None) 190 if attr_val is not None: 191 write_field(field, attr_val) 192 193 license = rfc822_escape(self.get_license()) 194 write_field('License', license) 195 for project_url in self.project_urls.items(): 196 write_field('Project-URL', '%s, %s' % project_url) 197 198 keywords = ','.join(self.get_keywords()) 199 if keywords: 200 write_field('Keywords', keywords) 201 202 for platform in self.get_platforms(): 203 write_field('Platform', platform) 204 205 self._write_list(file, 'Classifier', self.get_classifiers()) 206 207 # PEP 314 208 self._write_list(file, 'Requires', self.get_requires()) 209 self._write_list(file, 'Provides', self.get_provides()) 210 self._write_list(file, 'Obsoletes', self.get_obsoletes()) 211 212 # Setuptools specific for PEP 345 213 if hasattr(self, 'python_requires'): 214 write_field('Requires-Python', self.python_requires) 215 216 # PEP 566 217 if self.long_description_content_type: 218 write_field('Description-Content-Type', self.long_description_content_type) 219 if self.provides_extras: 220 for extra in self.provides_extras: 221 write_field('Provides-Extra', extra) 222 223 self._write_list(file, 'License-File', self.license_files or []) 224 225 file.write("\n%s\n\n" % self.get_long_description()) 226 227 228sequence = tuple, list 229 230 231def check_importable(dist, attr, value): 232 try: 233 ep = metadata.EntryPoint(value=value, name=None, group=None) 234 assert not ep.extras 235 except (TypeError, ValueError, AttributeError, AssertionError) as e: 236 raise DistutilsSetupError( 237 "%r must be importable 'module:attrs' string (got %r)" % (attr, value) 238 ) from e 239 240 241def assert_string_list(dist, attr, value): 242 """Verify that value is a string list""" 243 try: 244 # verify that value is a list or tuple to exclude unordered 245 # or single-use iterables 246 assert isinstance(value, (list, tuple)) 247 # verify that elements of value are strings 248 assert ''.join(value) != value 249 except (TypeError, ValueError, AttributeError, AssertionError) as e: 250 raise DistutilsSetupError( 251 "%r must be a list of strings (got %r)" % (attr, value) 252 ) from e 253 254 255def check_nsp(dist, attr, value): 256 """Verify that namespace packages are valid""" 257 ns_packages = value 258 assert_string_list(dist, attr, ns_packages) 259 for nsp in ns_packages: 260 if not dist.has_contents_for(nsp): 261 raise DistutilsSetupError( 262 "Distribution contains no modules or packages for " 263 + "namespace package %r" % nsp 264 ) 265 parent, sep, child = nsp.rpartition('.') 266 if parent and parent not in ns_packages: 267 distutils.log.warn( 268 "WARNING: %r is declared as a package namespace, but %r" 269 " is not: please correct this in setup.py", 270 nsp, 271 parent, 272 ) 273 274 275def check_extras(dist, attr, value): 276 """Verify that extras_require mapping is valid""" 277 try: 278 list(itertools.starmap(_check_extra, value.items())) 279 except (TypeError, ValueError, AttributeError) as e: 280 raise DistutilsSetupError( 281 "'extras_require' must be a dictionary whose values are " 282 "strings or lists of strings containing valid project/version " 283 "requirement specifiers." 284 ) from e 285 286 287def _check_extra(extra, reqs): 288 name, sep, marker = extra.partition(':') 289 if marker and pkg_resources.invalid_marker(marker): 290 raise DistutilsSetupError("Invalid environment marker: " + marker) 291 list(_reqs.parse(reqs)) 292 293 294def assert_bool(dist, attr, value): 295 """Verify that value is True, False, 0, or 1""" 296 if bool(value) != value: 297 tmpl = "{attr!r} must be a boolean value (got {value!r})" 298 raise DistutilsSetupError(tmpl.format(attr=attr, value=value)) 299 300 301def invalid_unless_false(dist, attr, value): 302 if not value: 303 warnings.warn(f"{attr} is ignored.", DistDeprecationWarning) 304 return 305 raise DistutilsSetupError(f"{attr} is invalid.") 306 307 308def check_requirements(dist, attr, value): 309 """Verify that install_requires is a valid requirements list""" 310 try: 311 list(_reqs.parse(value)) 312 if isinstance(value, (dict, set)): 313 raise TypeError("Unordered types are not allowed") 314 except (TypeError, ValueError) as error: 315 tmpl = ( 316 "{attr!r} must be a string or list of strings " 317 "containing valid project/version requirement specifiers; {error}" 318 ) 319 raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error 320 321 322def check_specifier(dist, attr, value): 323 """Verify that value is a valid version specifier""" 324 try: 325 packaging.specifiers.SpecifierSet(value) 326 except (packaging.specifiers.InvalidSpecifier, AttributeError) as error: 327 tmpl = ( 328 "{attr!r} must be a string " "containing valid version specifiers; {error}" 329 ) 330 raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error 331 332 333def check_entry_points(dist, attr, value): 334 """Verify that entry_points map is parseable""" 335 try: 336 _entry_points.load(value) 337 except Exception as e: 338 raise DistutilsSetupError(e) from e 339 340 341def check_test_suite(dist, attr, value): 342 if not isinstance(value, str): 343 raise DistutilsSetupError("test_suite must be a string") 344 345 346def check_package_data(dist, attr, value): 347 """Verify that value is a dictionary of package names to glob lists""" 348 if not isinstance(value, dict): 349 raise DistutilsSetupError( 350 "{!r} must be a dictionary mapping package names to lists of " 351 "string wildcard patterns".format(attr) 352 ) 353 for k, v in value.items(): 354 if not isinstance(k, str): 355 raise DistutilsSetupError( 356 "keys of {!r} dict must be strings (got {!r})".format(attr, k) 357 ) 358 assert_string_list(dist, 'values of {!r} dict'.format(attr), v) 359 360 361def check_packages(dist, attr, value): 362 for pkgname in value: 363 if not re.match(r'\w+(\.\w+)*', pkgname): 364 distutils.log.warn( 365 "WARNING: %r not a valid package name; please use only " 366 ".-separated package names in setup.py", 367 pkgname, 368 ) 369 370 371_Distribution = get_unpatched(distutils.core.Distribution) 372 373 374class Distribution(_Distribution): 375 """Distribution with support for tests and package data 376 377 This is an enhanced version of 'distutils.dist.Distribution' that 378 effectively adds the following new optional keyword arguments to 'setup()': 379 380 'install_requires' -- a string or sequence of strings specifying project 381 versions that the distribution requires when installed, in the format 382 used by 'pkg_resources.require()'. They will be installed 383 automatically when the package is installed. If you wish to use 384 packages that are not available in PyPI, or want to give your users an 385 alternate download location, you can add a 'find_links' option to the 386 '[easy_install]' section of your project's 'setup.cfg' file, and then 387 setuptools will scan the listed web pages for links that satisfy the 388 requirements. 389 390 'extras_require' -- a dictionary mapping names of optional "extras" to the 391 additional requirement(s) that using those extras incurs. For example, 392 this:: 393 394 extras_require = dict(reST = ["docutils>=0.3", "reSTedit"]) 395 396 indicates that the distribution can optionally provide an extra 397 capability called "reST", but it can only be used if docutils and 398 reSTedit are installed. If the user installs your package using 399 EasyInstall and requests one of your extras, the corresponding 400 additional requirements will be installed if needed. 401 402 'test_suite' -- the name of a test suite to run for the 'test' command. 403 If the user runs 'python setup.py test', the package will be installed, 404 and the named test suite will be run. The format is the same as 405 would be used on a 'unittest.py' command line. That is, it is the 406 dotted name of an object to import and call to generate a test suite. 407 408 'package_data' -- a dictionary mapping package names to lists of filenames 409 or globs to use to find data files contained in the named packages. 410 If the dictionary has filenames or globs listed under '""' (the empty 411 string), those names will be searched for in every package, in addition 412 to any names for the specific package. Data files found using these 413 names/globs will be installed along with the package, in the same 414 location as the package. Note that globs are allowed to reference 415 the contents of non-package subdirectories, as long as you use '/' as 416 a path separator. (Globs are automatically converted to 417 platform-specific paths at runtime.) 418 419 In addition to these new keywords, this class also has several new methods 420 for manipulating the distribution's contents. For example, the 'include()' 421 and 'exclude()' methods can be thought of as in-place add and subtract 422 commands that add or remove packages, modules, extensions, and so on from 423 the distribution. 424 """ 425 426 _DISTUTILS_UNSUPPORTED_METADATA = { 427 'long_description_content_type': lambda: None, 428 'project_urls': dict, 429 'provides_extras': ordered_set.OrderedSet, 430 'license_file': lambda: None, 431 'license_files': lambda: None, 432 } 433 434 _patched_dist = None 435 436 def patch_missing_pkg_info(self, attrs): 437 # Fake up a replacement for the data that would normally come from 438 # PKG-INFO, but which might not yet be built if this is a fresh 439 # checkout. 440 # 441 if not attrs or 'name' not in attrs or 'version' not in attrs: 442 return 443 key = pkg_resources.safe_name(str(attrs['name'])).lower() 444 dist = pkg_resources.working_set.by_key.get(key) 445 if dist is not None and not dist.has_metadata('PKG-INFO'): 446 dist._version = pkg_resources.safe_version(str(attrs['version'])) 447 self._patched_dist = dist 448 449 def __init__(self, attrs=None): 450 have_package_data = hasattr(self, "package_data") 451 if not have_package_data: 452 self.package_data = {} 453 attrs = attrs or {} 454 self.dist_files = [] 455 # Filter-out setuptools' specific options. 456 self.src_root = attrs.pop("src_root", None) 457 self.patch_missing_pkg_info(attrs) 458 self.dependency_links = attrs.pop('dependency_links', []) 459 self.setup_requires = attrs.pop('setup_requires', []) 460 for ep in metadata.entry_points(group='distutils.setup_keywords'): 461 vars(self).setdefault(ep.name, None) 462 _Distribution.__init__( 463 self, 464 { 465 k: v 466 for k, v in attrs.items() 467 if k not in self._DISTUTILS_UNSUPPORTED_METADATA 468 }, 469 ) 470 471 self.set_defaults = ConfigDiscovery(self) 472 473 self._set_metadata_defaults(attrs) 474 475 self.metadata.version = self._normalize_version( 476 self._validate_version(self.metadata.version) 477 ) 478 self._finalize_requires() 479 480 def _validate_metadata(self): 481 required = {"name"} 482 provided = { 483 key 484 for key in vars(self.metadata) 485 if getattr(self.metadata, key, None) is not None 486 } 487 missing = required - provided 488 489 if missing: 490 msg = f"Required package metadata is missing: {missing}" 491 raise DistutilsSetupError(msg) 492 493 def _set_metadata_defaults(self, attrs): 494 """ 495 Fill-in missing metadata fields not supported by distutils. 496 Some fields may have been set by other tools (e.g. pbr). 497 Those fields (vars(self.metadata)) take precedence to 498 supplied attrs. 499 """ 500 for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items(): 501 vars(self.metadata).setdefault(option, attrs.get(option, default())) 502 503 @staticmethod 504 def _normalize_version(version): 505 if isinstance(version, setuptools.sic) or version is None: 506 return version 507 508 normalized = str(packaging.version.Version(version)) 509 if version != normalized: 510 tmpl = "Normalizing '{version}' to '{normalized}'" 511 warnings.warn(tmpl.format(**locals())) 512 return normalized 513 return version 514 515 @staticmethod 516 def _validate_version(version): 517 if isinstance(version, numbers.Number): 518 # Some people apparently take "version number" too literally :) 519 version = str(version) 520 521 if version is not None: 522 try: 523 packaging.version.Version(version) 524 except (packaging.version.InvalidVersion, TypeError): 525 warnings.warn( 526 "The version specified (%r) is an invalid version, this " 527 "may not work as expected with newer versions of " 528 "setuptools, pip, and PyPI. Please see PEP 440 for more " 529 "details." % version 530 ) 531 return setuptools.sic(version) 532 return version 533 534 def _finalize_requires(self): 535 """ 536 Set `metadata.python_requires` and fix environment markers 537 in `install_requires` and `extras_require`. 538 """ 539 if getattr(self, 'python_requires', None): 540 self.metadata.python_requires = self.python_requires 541 542 if getattr(self, 'extras_require', None): 543 for extra in self.extras_require.keys(): 544 # Since this gets called multiple times at points where the 545 # keys have become 'converted' extras, ensure that we are only 546 # truly adding extras we haven't seen before here. 547 extra = extra.split(':')[0] 548 if extra: 549 self.metadata.provides_extras.add(extra) 550 551 self._convert_extras_requirements() 552 self._move_install_requirements_markers() 553 554 def _convert_extras_requirements(self): 555 """ 556 Convert requirements in `extras_require` of the form 557 `"extra": ["barbazquux; {marker}"]` to 558 `"extra:{marker}": ["barbazquux"]`. 559 """ 560 spec_ext_reqs = getattr(self, 'extras_require', None) or {} 561 self._tmp_extras_require = defaultdict(list) 562 for section, v in spec_ext_reqs.items(): 563 # Do not strip empty sections. 564 self._tmp_extras_require[section] 565 for r in _reqs.parse(v): 566 suffix = self._suffix_for(r) 567 self._tmp_extras_require[section + suffix].append(r) 568 569 @staticmethod 570 def _suffix_for(req): 571 """ 572 For a requirement, return the 'extras_require' suffix for 573 that requirement. 574 """ 575 return ':' + str(req.marker) if req.marker else '' 576 577 def _move_install_requirements_markers(self): 578 """ 579 Move requirements in `install_requires` that are using environment 580 markers `extras_require`. 581 """ 582 583 # divide the install_requires into two sets, simple ones still 584 # handled by install_requires and more complex ones handled 585 # by extras_require. 586 587 def is_simple_req(req): 588 return not req.marker 589 590 spec_inst_reqs = getattr(self, 'install_requires', None) or () 591 inst_reqs = list(_reqs.parse(spec_inst_reqs)) 592 simple_reqs = filter(is_simple_req, inst_reqs) 593 complex_reqs = itertools.filterfalse(is_simple_req, inst_reqs) 594 self.install_requires = list(map(str, simple_reqs)) 595 596 for r in complex_reqs: 597 self._tmp_extras_require[':' + str(r.marker)].append(r) 598 self.extras_require = dict( 599 (k, [str(r) for r in map(self._clean_req, v)]) 600 for k, v in self._tmp_extras_require.items() 601 ) 602 603 def _clean_req(self, req): 604 """ 605 Given a Requirement, remove environment markers and return it. 606 """ 607 req.marker = None 608 return req 609 610 def _finalize_license_files(self): 611 """Compute names of all license files which should be included.""" 612 license_files: Optional[List[str]] = self.metadata.license_files 613 patterns: List[str] = license_files if license_files else [] 614 615 license_file: Optional[str] = self.metadata.license_file 616 if license_file and license_file not in patterns: 617 patterns.append(license_file) 618 619 if license_files is None and license_file is None: 620 # Default patterns match the ones wheel uses 621 # See https://wheel.readthedocs.io/en/stable/user_guide.html 622 # -> 'Including license files in the generated wheel file' 623 patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') 624 625 self.metadata.license_files = list( 626 unique_everseen(self._expand_patterns(patterns)) 627 ) 628 629 @staticmethod 630 def _expand_patterns(patterns): 631 """ 632 >>> list(Distribution._expand_patterns(['LICENSE'])) 633 ['LICENSE'] 634 >>> list(Distribution._expand_patterns(['setup.cfg', 'LIC*'])) 635 ['setup.cfg', 'LICENSE'] 636 """ 637 return ( 638 path 639 for pattern in patterns 640 for path in sorted(iglob(pattern)) 641 if not path.endswith('~') and os.path.isfile(path) 642 ) 643 644 # FIXME: 'Distribution._parse_config_files' is too complex (14) 645 def _parse_config_files(self, filenames=None): # noqa: C901 646 """ 647 Adapted from distutils.dist.Distribution.parse_config_files, 648 this method provides the same functionality in subtly-improved 649 ways. 650 """ 651 from configparser import ConfigParser 652 653 # Ignore install directory options if we have a venv 654 ignore_options = ( 655 [] 656 if sys.prefix == sys.base_prefix 657 else [ 658 'install-base', 659 'install-platbase', 660 'install-lib', 661 'install-platlib', 662 'install-purelib', 663 'install-headers', 664 'install-scripts', 665 'install-data', 666 'prefix', 667 'exec-prefix', 668 'home', 669 'user', 670 'root', 671 ] 672 ) 673 674 ignore_options = frozenset(ignore_options) 675 676 if filenames is None: 677 filenames = self.find_config_files() 678 679 if DEBUG: 680 self.announce("Distribution.parse_config_files():") 681 682 parser = ConfigParser() 683 parser.optionxform = str 684 for filename in filenames: 685 with io.open(filename, encoding='utf-8') as reader: 686 if DEBUG: 687 self.announce(" reading {filename}".format(**locals())) 688 parser.read_file(reader) 689 for section in parser.sections(): 690 options = parser.options(section) 691 opt_dict = self.get_option_dict(section) 692 693 for opt in options: 694 if opt == '__name__' or opt in ignore_options: 695 continue 696 697 val = parser.get(section, opt) 698 opt = self.warn_dash_deprecation(opt, section) 699 opt = self.make_option_lowercase(opt, section) 700 opt_dict[opt] = (filename, val) 701 702 # Make the ConfigParser forget everything (so we retain 703 # the original filenames that options come from) 704 parser.__init__() 705 706 if 'global' not in self.command_options: 707 return 708 709 # If there was a "global" section in the config file, use it 710 # to set Distribution options. 711 712 for (opt, (src, val)) in self.command_options['global'].items(): 713 alias = self.negative_opt.get(opt) 714 if alias: 715 val = not strtobool(val) 716 elif opt in ('verbose', 'dry_run'): # ugh! 717 val = strtobool(val) 718 719 try: 720 setattr(self, alias or opt, val) 721 except ValueError as e: 722 raise DistutilsOptionError(e) from e 723 724 def warn_dash_deprecation(self, opt, section): 725 if section in ( 726 'options.extras_require', 727 'options.data_files', 728 ): 729 return opt 730 731 underscore_opt = opt.replace('-', '_') 732 commands = list(itertools.chain( 733 distutils.command.__all__, 734 self._setuptools_commands(), 735 )) 736 if ( 737 not section.startswith('options') 738 and section != 'metadata' 739 and section not in commands 740 ): 741 return underscore_opt 742 743 if '-' in opt: 744 warnings.warn( 745 "Usage of dash-separated '%s' will not be supported in future " 746 "versions. Please use the underscore name '%s' instead" 747 % (opt, underscore_opt) 748 ) 749 return underscore_opt 750 751 def _setuptools_commands(self): 752 try: 753 return metadata.distribution('setuptools').entry_points.names 754 except metadata.PackageNotFoundError: 755 # during bootstrapping, distribution doesn't exist 756 return [] 757 758 def make_option_lowercase(self, opt, section): 759 if section != 'metadata' or opt.islower(): 760 return opt 761 762 lowercase_opt = opt.lower() 763 warnings.warn( 764 "Usage of uppercase key '%s' in '%s' will be deprecated in future " 765 "versions. Please use lowercase '%s' instead" 766 % (opt, section, lowercase_opt) 767 ) 768 return lowercase_opt 769 770 # FIXME: 'Distribution._set_command_options' is too complex (14) 771 def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 772 """ 773 Set the options for 'command_obj' from 'option_dict'. Basically 774 this means copying elements of a dictionary ('option_dict') to 775 attributes of an instance ('command'). 776 777 'command_obj' must be a Command instance. If 'option_dict' is not 778 supplied, uses the standard option dictionary for this command 779 (from 'self.command_options'). 780 781 (Adopted from distutils.dist.Distribution._set_command_options) 782 """ 783 command_name = command_obj.get_command_name() 784 if option_dict is None: 785 option_dict = self.get_option_dict(command_name) 786 787 if DEBUG: 788 self.announce(" setting options for '%s' command:" % command_name) 789 for (option, (source, value)) in option_dict.items(): 790 if DEBUG: 791 self.announce(" %s = %s (from %s)" % (option, value, source)) 792 try: 793 bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] 794 except AttributeError: 795 bool_opts = [] 796 try: 797 neg_opt = command_obj.negative_opt 798 except AttributeError: 799 neg_opt = {} 800 801 try: 802 is_string = isinstance(value, str) 803 if option in neg_opt and is_string: 804 setattr(command_obj, neg_opt[option], not strtobool(value)) 805 elif option in bool_opts and is_string: 806 setattr(command_obj, option, strtobool(value)) 807 elif hasattr(command_obj, option): 808 setattr(command_obj, option, value) 809 else: 810 raise DistutilsOptionError( 811 "error in %s: command '%s' has no such option '%s'" 812 % (source, command_name, option) 813 ) 814 except ValueError as e: 815 raise DistutilsOptionError(e) from e 816 817 def parse_config_files(self, filenames=None, ignore_option_errors=False): 818 """Parses configuration files from various levels 819 and loads configuration. 820 """ 821 tomlfiles = [] 822 standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml") 823 if filenames is not None: 824 parts = partition(lambda f: Path(f).suffix == ".toml", filenames) 825 filenames = list(parts[0]) # 1st element => predicate is False 826 tomlfiles = list(parts[1]) # 2nd element => predicate is True 827 elif standard_project_metadata.exists(): 828 tomlfiles = [standard_project_metadata] 829 830 self._parse_config_files(filenames=filenames) 831 832 setupcfg.parse_configuration( 833 self, self.command_options, ignore_option_errors=ignore_option_errors 834 ) 835 for filename in tomlfiles: 836 pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) 837 838 self._finalize_requires() 839 self._finalize_license_files() 840 841 def fetch_build_eggs(self, requires): 842 """Resolve pre-setup requirements""" 843 resolved_dists = pkg_resources.working_set.resolve( 844 _reqs.parse(requires), 845 installer=self.fetch_build_egg, 846 replace_conflicting=True, 847 ) 848 for dist in resolved_dists: 849 pkg_resources.working_set.add(dist, replace=True) 850 return resolved_dists 851 852 def finalize_options(self): 853 """ 854 Allow plugins to apply arbitrary operations to the 855 distribution. Each hook may optionally define a 'order' 856 to influence the order of execution. Smaller numbers 857 go first and the default is 0. 858 """ 859 group = 'setuptools.finalize_distribution_options' 860 861 def by_order(hook): 862 return getattr(hook, 'order', 0) 863 864 defined = metadata.entry_points(group=group) 865 filtered = itertools.filterfalse(self._removed, defined) 866 loaded = map(lambda e: e.load(), filtered) 867 for ep in sorted(loaded, key=by_order): 868 ep(self) 869 870 @staticmethod 871 def _removed(ep): 872 """ 873 When removing an entry point, if metadata is loaded 874 from an older version of Setuptools, that removed 875 entry point will attempt to be loaded and will fail. 876 See #2765 for more details. 877 """ 878 removed = { 879 # removed 2021-09-05 880 '2to3_doctests', 881 } 882 return ep.name in removed 883 884 def _finalize_setup_keywords(self): 885 for ep in metadata.entry_points(group='distutils.setup_keywords'): 886 value = getattr(self, ep.name, None) 887 if value is not None: 888 self._install_dependencies(ep) 889 ep.load()(self, ep.name, value) 890 891 def _install_dependencies(self, ep): 892 """ 893 Given an entry point, ensure that any declared extras for 894 its distribution are installed. 895 """ 896 for req in nspektr.missing(ep): 897 # fetch_build_egg expects pkg_resources.Requirement 898 self.fetch_build_egg(pkg_resources.Requirement(str(req))) 899 900 def get_egg_cache_dir(self): 901 egg_cache_dir = os.path.join(os.curdir, '.eggs') 902 if not os.path.exists(egg_cache_dir): 903 os.mkdir(egg_cache_dir) 904 windows_support.hide_file(egg_cache_dir) 905 readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt') 906 with open(readme_txt_filename, 'w') as f: 907 f.write( 908 'This directory contains eggs that were downloaded ' 909 'by setuptools to build, test, and run plug-ins.\n\n' 910 ) 911 f.write( 912 'This directory caches those eggs to prevent ' 913 'repeated downloads.\n\n' 914 ) 915 f.write('However, it is safe to delete this directory.\n\n') 916 917 return egg_cache_dir 918 919 def fetch_build_egg(self, req): 920 """Fetch an egg needed for building""" 921 from setuptools.installer import fetch_build_egg 922 923 return fetch_build_egg(self, req) 924 925 def get_command_class(self, command): 926 """Pluggable version of get_command_class()""" 927 if command in self.cmdclass: 928 return self.cmdclass[command] 929 930 eps = metadata.entry_points(group='distutils.commands', name=command) 931 for ep in eps: 932 self._install_dependencies(ep) 933 self.cmdclass[command] = cmdclass = ep.load() 934 return cmdclass 935 else: 936 return _Distribution.get_command_class(self, command) 937 938 def print_commands(self): 939 for ep in metadata.entry_points(group='distutils.commands'): 940 if ep.name not in self.cmdclass: 941 cmdclass = ep.load() 942 self.cmdclass[ep.name] = cmdclass 943 return _Distribution.print_commands(self) 944 945 def get_command_list(self): 946 for ep in metadata.entry_points(group='distutils.commands'): 947 if ep.name not in self.cmdclass: 948 cmdclass = ep.load() 949 self.cmdclass[ep.name] = cmdclass 950 return _Distribution.get_command_list(self) 951 952 def include(self, **attrs): 953 """Add items to distribution that are named in keyword arguments 954 955 For example, 'dist.include(py_modules=["x"])' would add 'x' to 956 the distribution's 'py_modules' attribute, if it was not already 957 there. 958 959 Currently, this method only supports inclusion for attributes that are 960 lists or tuples. If you need to add support for adding to other 961 attributes in this or a subclass, you can add an '_include_X' method, 962 where 'X' is the name of the attribute. The method will be called with 963 the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})' 964 will try to call 'dist._include_foo({"bar":"baz"})', which can then 965 handle whatever special inclusion logic is needed. 966 """ 967 for k, v in attrs.items(): 968 include = getattr(self, '_include_' + k, None) 969 if include: 970 include(v) 971 else: 972 self._include_misc(k, v) 973 974 def exclude_package(self, package): 975 """Remove packages, modules, and extensions in named package""" 976 977 pfx = package + '.' 978 if self.packages: 979 self.packages = [ 980 p for p in self.packages if p != package and not p.startswith(pfx) 981 ] 982 983 if self.py_modules: 984 self.py_modules = [ 985 p for p in self.py_modules if p != package and not p.startswith(pfx) 986 ] 987 988 if self.ext_modules: 989 self.ext_modules = [ 990 p 991 for p in self.ext_modules 992 if p.name != package and not p.name.startswith(pfx) 993 ] 994 995 def has_contents_for(self, package): 996 """Return true if 'exclude_package(package)' would do something""" 997 998 pfx = package + '.' 999 1000 for p in self.iter_distribution_names(): 1001 if p == package or p.startswith(pfx): 1002 return True 1003 1004 def _exclude_misc(self, name, value): 1005 """Handle 'exclude()' for list/tuple attrs without a special handler""" 1006 if not isinstance(value, sequence): 1007 raise DistutilsSetupError( 1008 "%s: setting must be a list or tuple (%r)" % (name, value) 1009 ) 1010 try: 1011 old = getattr(self, name) 1012 except AttributeError as e: 1013 raise DistutilsSetupError("%s: No such distribution setting" % name) from e 1014 if old is not None and not isinstance(old, sequence): 1015 raise DistutilsSetupError( 1016 name + ": this setting cannot be changed via include/exclude" 1017 ) 1018 elif old: 1019 setattr(self, name, [item for item in old if item not in value]) 1020 1021 def _include_misc(self, name, value): 1022 """Handle 'include()' for list/tuple attrs without a special handler""" 1023 1024 if not isinstance(value, sequence): 1025 raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value)) 1026 try: 1027 old = getattr(self, name) 1028 except AttributeError as e: 1029 raise DistutilsSetupError("%s: No such distribution setting" % name) from e 1030 if old is None: 1031 setattr(self, name, value) 1032 elif not isinstance(old, sequence): 1033 raise DistutilsSetupError( 1034 name + ": this setting cannot be changed via include/exclude" 1035 ) 1036 else: 1037 new = [item for item in value if item not in old] 1038 setattr(self, name, old + new) 1039 1040 def exclude(self, **attrs): 1041 """Remove items from distribution that are named in keyword arguments 1042 1043 For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from 1044 the distribution's 'py_modules' attribute. Excluding packages uses 1045 the 'exclude_package()' method, so all of the package's contained 1046 packages, modules, and extensions are also excluded. 1047 1048 Currently, this method only supports exclusion from attributes that are 1049 lists or tuples. If you need to add support for excluding from other 1050 attributes in this or a subclass, you can add an '_exclude_X' method, 1051 where 'X' is the name of the attribute. The method will be called with 1052 the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})' 1053 will try to call 'dist._exclude_foo({"bar":"baz"})', which can then 1054 handle whatever special exclusion logic is needed. 1055 """ 1056 for k, v in attrs.items(): 1057 exclude = getattr(self, '_exclude_' + k, None) 1058 if exclude: 1059 exclude(v) 1060 else: 1061 self._exclude_misc(k, v) 1062 1063 def _exclude_packages(self, packages): 1064 if not isinstance(packages, sequence): 1065 raise DistutilsSetupError( 1066 "packages: setting must be a list or tuple (%r)" % (packages,) 1067 ) 1068 list(map(self.exclude_package, packages)) 1069 1070 def _parse_command_opts(self, parser, args): 1071 # Remove --with-X/--without-X options when processing command args 1072 self.global_options = self.__class__.global_options 1073 self.negative_opt = self.__class__.negative_opt 1074 1075 # First, expand any aliases 1076 command = args[0] 1077 aliases = self.get_option_dict('aliases') 1078 while command in aliases: 1079 src, alias = aliases[command] 1080 del aliases[command] # ensure each alias can expand only once! 1081 import shlex 1082 1083 args[:1] = shlex.split(alias, True) 1084 command = args[0] 1085 1086 nargs = _Distribution._parse_command_opts(self, parser, args) 1087 1088 # Handle commands that want to consume all remaining arguments 1089 cmd_class = self.get_command_class(command) 1090 if getattr(cmd_class, 'command_consumes_arguments', None): 1091 self.get_option_dict(command)['args'] = ("command line", nargs) 1092 if nargs is not None: 1093 return [] 1094 1095 return nargs 1096 1097 def get_cmdline_options(self): 1098 """Return a '{cmd: {opt:val}}' map of all command-line options 1099 1100 Option names are all long, but do not include the leading '--', and 1101 contain dashes rather than underscores. If the option doesn't take 1102 an argument (e.g. '--quiet'), the 'val' is 'None'. 1103 1104 Note that options provided by config files are intentionally excluded. 1105 """ 1106 1107 d = {} 1108 1109 for cmd, opts in self.command_options.items(): 1110 1111 for opt, (src, val) in opts.items(): 1112 1113 if src != "command line": 1114 continue 1115 1116 opt = opt.replace('_', '-') 1117 1118 if val == 0: 1119 cmdobj = self.get_command_obj(cmd) 1120 neg_opt = self.negative_opt.copy() 1121 neg_opt.update(getattr(cmdobj, 'negative_opt', {})) 1122 for neg, pos in neg_opt.items(): 1123 if pos == opt: 1124 opt = neg 1125 val = None 1126 break 1127 else: 1128 raise AssertionError("Shouldn't be able to get here") 1129 1130 elif val == 1: 1131 val = None 1132 1133 d.setdefault(cmd, {})[opt] = val 1134 1135 return d 1136 1137 def iter_distribution_names(self): 1138 """Yield all packages, modules, and extension names in distribution""" 1139 1140 for pkg in self.packages or (): 1141 yield pkg 1142 1143 for module in self.py_modules or (): 1144 yield module 1145 1146 for ext in self.ext_modules or (): 1147 if isinstance(ext, tuple): 1148 name, buildinfo = ext 1149 else: 1150 name = ext.name 1151 if name.endswith('module'): 1152 name = name[:-6] 1153 yield name 1154 1155 def handle_display_options(self, option_order): 1156 """If there were any non-global "display-only" options 1157 (--help-commands or the metadata display options) on the command 1158 line, display the requested info and return true; else return 1159 false. 1160 """ 1161 import sys 1162 1163 if self.help_commands: 1164 return _Distribution.handle_display_options(self, option_order) 1165 1166 # Stdout may be StringIO (e.g. in tests) 1167 if not isinstance(sys.stdout, io.TextIOWrapper): 1168 return _Distribution.handle_display_options(self, option_order) 1169 1170 # Don't wrap stdout if utf-8 is already the encoding. Provides 1171 # workaround for #334. 1172 if sys.stdout.encoding.lower() in ('utf-8', 'utf8'): 1173 return _Distribution.handle_display_options(self, option_order) 1174 1175 # Print metadata in UTF-8 no matter the platform 1176 encoding = sys.stdout.encoding 1177 errors = sys.stdout.errors 1178 newline = sys.platform != 'win32' and '\n' or None 1179 line_buffering = sys.stdout.line_buffering 1180 1181 sys.stdout = io.TextIOWrapper( 1182 sys.stdout.detach(), 'utf-8', errors, newline, line_buffering 1183 ) 1184 try: 1185 return _Distribution.handle_display_options(self, option_order) 1186 finally: 1187 sys.stdout = io.TextIOWrapper( 1188 sys.stdout.detach(), encoding, errors, newline, line_buffering 1189 ) 1190 1191 def run_command(self, command): 1192 self.set_defaults() 1193 # Postpone defaults until all explicit configuration is considered 1194 # (setup() args, config files, command line and plugins) 1195 1196 super().run_command(command) 1197 1198 1199class DistDeprecationWarning(SetuptoolsDeprecationWarning): 1200 """Class for warning about deprecations in dist in 1201 setuptools. Not ignored by default, unlike DeprecationWarning.""" 1202