Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/setuptools/dist.py: 19%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import io
4import itertools
5import numbers
6import os
7import re
8import sys
9from glob import iglob
10from pathlib import Path
11from typing import TYPE_CHECKING, MutableMapping
13from more_itertools import partition, unique_everseen
14from packaging.markers import InvalidMarker, Marker
15from packaging.specifiers import InvalidSpecifier, SpecifierSet
16from packaging.version import Version
18from . import (
19 _entry_points,
20 _reqs,
21 command as _, # noqa: F401 # imported for side-effects
22)
23from ._importlib import metadata
24from .config import pyprojecttoml, setupcfg
25from .discovery import ConfigDiscovery
26from .monkey import get_unpatched
27from .warnings import InformationOnly, SetuptoolsDeprecationWarning
29import distutils.cmd
30import distutils.command
31import distutils.core
32import distutils.dist
33import distutils.log
34from distutils.debug import DEBUG
35from distutils.errors import DistutilsOptionError, DistutilsSetupError
36from distutils.fancy_getopt import translate_longopt
37from distutils.util import strtobool
39__all__ = ['Distribution']
41sequence = tuple, list
44def check_importable(dist, attr, value):
45 try:
46 ep = metadata.EntryPoint(value=value, name=None, group=None)
47 assert not ep.extras
48 except (TypeError, ValueError, AttributeError, AssertionError) as e:
49 raise DistutilsSetupError(
50 "%r must be importable 'module:attrs' string (got %r)" % (attr, value)
51 ) from e
54def assert_string_list(dist, attr, value):
55 """Verify that value is a string list"""
56 try:
57 # verify that value is a list or tuple to exclude unordered
58 # or single-use iterables
59 assert isinstance(value, sequence)
60 # verify that elements of value are strings
61 assert ''.join(value) != value
62 except (TypeError, ValueError, AttributeError, AssertionError) as e:
63 raise DistutilsSetupError(
64 "%r must be a list of strings (got %r)" % (attr, value)
65 ) from e
68def check_nsp(dist, attr, value):
69 """Verify that namespace packages are valid"""
70 ns_packages = value
71 assert_string_list(dist, attr, ns_packages)
72 for nsp in ns_packages:
73 if not dist.has_contents_for(nsp):
74 raise DistutilsSetupError(
75 "Distribution contains no modules or packages for "
76 + "namespace package %r" % nsp
77 )
78 parent, sep, child = nsp.rpartition('.')
79 if parent and parent not in ns_packages:
80 distutils.log.warn(
81 "WARNING: %r is declared as a package namespace, but %r"
82 " is not: please correct this in setup.py",
83 nsp,
84 parent,
85 )
86 SetuptoolsDeprecationWarning.emit(
87 "The namespace_packages parameter is deprecated.",
88 "Please replace its usage with implicit namespaces (PEP 420).",
89 see_docs="references/keywords.html#keyword-namespace-packages",
90 # TODO: define due_date, it may break old packages that are no longer
91 # maintained (e.g. sphinxcontrib extensions) when installed from source.
92 # Warning officially introduced in May 2022, however the deprecation
93 # was mentioned much earlier in the docs (May 2020, see #2149).
94 )
97def check_extras(dist, attr, value):
98 """Verify that extras_require mapping is valid"""
99 try:
100 list(itertools.starmap(_check_extra, value.items()))
101 except (TypeError, ValueError, AttributeError) as e:
102 raise DistutilsSetupError(
103 "'extras_require' must be a dictionary whose values are "
104 "strings or lists of strings containing valid project/version "
105 "requirement specifiers."
106 ) from e
109def _check_extra(extra, reqs):
110 name, sep, marker = extra.partition(':')
111 try:
112 _check_marker(marker)
113 except InvalidMarker:
114 msg = f"Invalid environment marker: {marker} ({extra!r})"
115 raise DistutilsSetupError(msg) from None
116 list(_reqs.parse(reqs))
119def _check_marker(marker):
120 if not marker:
121 return
122 m = Marker(marker)
123 m.evaluate()
126def assert_bool(dist, attr, value):
127 """Verify that value is True, False, 0, or 1"""
128 if bool(value) != value:
129 tmpl = "{attr!r} must be a boolean value (got {value!r})"
130 raise DistutilsSetupError(tmpl.format(attr=attr, value=value))
133def invalid_unless_false(dist, attr, value):
134 if not value:
135 DistDeprecationWarning.emit(f"{attr} is ignored.")
136 # TODO: should there be a `due_date` here?
137 return
138 raise DistutilsSetupError(f"{attr} is invalid.")
141def check_requirements(dist, attr, value):
142 """Verify that install_requires is a valid requirements list"""
143 try:
144 list(_reqs.parse(value))
145 if isinstance(value, (dict, set)):
146 raise TypeError("Unordered types are not allowed")
147 except (TypeError, ValueError) as error:
148 tmpl = (
149 "{attr!r} must be a string or list of strings "
150 "containing valid project/version requirement specifiers; {error}"
151 )
152 raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
155def check_specifier(dist, attr, value):
156 """Verify that value is a valid version specifier"""
157 try:
158 SpecifierSet(value)
159 except (InvalidSpecifier, AttributeError) as error:
160 tmpl = "{attr!r} must be a string containing valid version specifiers; {error}"
161 raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
164def check_entry_points(dist, attr, value):
165 """Verify that entry_points map is parseable"""
166 try:
167 _entry_points.load(value)
168 except Exception as e:
169 raise DistutilsSetupError(e) from e
172def check_package_data(dist, attr, value):
173 """Verify that value is a dictionary of package names to glob lists"""
174 if not isinstance(value, dict):
175 raise DistutilsSetupError(
176 "{!r} must be a dictionary mapping package names to lists of "
177 "string wildcard patterns".format(attr)
178 )
179 for k, v in value.items():
180 if not isinstance(k, str):
181 raise DistutilsSetupError(
182 "keys of {!r} dict must be strings (got {!r})".format(attr, k)
183 )
184 assert_string_list(dist, 'values of {!r} dict'.format(attr), v)
187def check_packages(dist, attr, value):
188 for pkgname in value:
189 if not re.match(r'\w+(\.\w+)*', pkgname):
190 distutils.log.warn(
191 "WARNING: %r not a valid package name; please use only "
192 ".-separated package names in setup.py",
193 pkgname,
194 )
197if TYPE_CHECKING:
198 from typing_extensions import TypeAlias
200 # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962
201 _Distribution: TypeAlias = distutils.core.Distribution
202else:
203 _Distribution = get_unpatched(distutils.core.Distribution)
206class Distribution(_Distribution):
207 """Distribution with support for tests and package data
209 This is an enhanced version of 'distutils.dist.Distribution' that
210 effectively adds the following new optional keyword arguments to 'setup()':
212 'install_requires' -- a string or sequence of strings specifying project
213 versions that the distribution requires when installed, in the format
214 used by 'pkg_resources.require()'. They will be installed
215 automatically when the package is installed. If you wish to use
216 packages that are not available in PyPI, or want to give your users an
217 alternate download location, you can add a 'find_links' option to the
218 '[easy_install]' section of your project's 'setup.cfg' file, and then
219 setuptools will scan the listed web pages for links that satisfy the
220 requirements.
222 'extras_require' -- a dictionary mapping names of optional "extras" to the
223 additional requirement(s) that using those extras incurs. For example,
224 this::
226 extras_require = dict(reST = ["docutils>=0.3", "reSTedit"])
228 indicates that the distribution can optionally provide an extra
229 capability called "reST", but it can only be used if docutils and
230 reSTedit are installed. If the user installs your package using
231 EasyInstall and requests one of your extras, the corresponding
232 additional requirements will be installed if needed.
234 'package_data' -- a dictionary mapping package names to lists of filenames
235 or globs to use to find data files contained in the named packages.
236 If the dictionary has filenames or globs listed under '""' (the empty
237 string), those names will be searched for in every package, in addition
238 to any names for the specific package. Data files found using these
239 names/globs will be installed along with the package, in the same
240 location as the package. Note that globs are allowed to reference
241 the contents of non-package subdirectories, as long as you use '/' as
242 a path separator. (Globs are automatically converted to
243 platform-specific paths at runtime.)
245 In addition to these new keywords, this class also has several new methods
246 for manipulating the distribution's contents. For example, the 'include()'
247 and 'exclude()' methods can be thought of as in-place add and subtract
248 commands that add or remove packages, modules, extensions, and so on from
249 the distribution.
250 """
252 _DISTUTILS_UNSUPPORTED_METADATA = {
253 'long_description_content_type': lambda: None,
254 'project_urls': dict,
255 'provides_extras': dict, # behaves like an ordered set
256 'license_file': lambda: None,
257 'license_files': lambda: None,
258 'install_requires': list,
259 'extras_require': dict,
260 }
262 # Used by build_py, editable_wheel and install_lib commands for legacy namespaces
263 namespace_packages: list[str] #: :meta private: DEPRECATED
265 def __init__(self, attrs: MutableMapping | None = None) -> None:
266 have_package_data = hasattr(self, "package_data")
267 if not have_package_data:
268 self.package_data: dict[str, list[str]] = {}
269 attrs = attrs or {}
270 self.dist_files: list[tuple[str, str, str]] = []
271 self.include_package_data: bool | None = None
272 self.exclude_package_data: dict[str, list[str]] | None = None
273 # Filter-out setuptools' specific options.
274 self.src_root = attrs.pop("src_root", None)
275 self.dependency_links = attrs.pop('dependency_links', [])
276 self.setup_requires = attrs.pop('setup_requires', [])
277 for ep in metadata.entry_points(group='distutils.setup_keywords'):
278 vars(self).setdefault(ep.name, None)
280 metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA)
281 metadata_only -= {"install_requires", "extras_require"}
282 dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only}
283 _Distribution.__init__(self, dist_attrs)
285 # Private API (setuptools-use only, not restricted to Distribution)
286 # Stores files that are referenced by the configuration and need to be in the
287 # sdist (e.g. `version = file: VERSION.txt`)
288 self._referenced_files: set[str] = set()
290 self.set_defaults = ConfigDiscovery(self)
292 self._set_metadata_defaults(attrs)
294 self.metadata.version = self._normalize_version(self.metadata.version)
295 self._finalize_requires()
297 def _validate_metadata(self):
298 required = {"name"}
299 provided = {
300 key
301 for key in vars(self.metadata)
302 if getattr(self.metadata, key, None) is not None
303 }
304 missing = required - provided
306 if missing:
307 msg = f"Required package metadata is missing: {missing}"
308 raise DistutilsSetupError(msg)
310 def _set_metadata_defaults(self, attrs):
311 """
312 Fill-in missing metadata fields not supported by distutils.
313 Some fields may have been set by other tools (e.g. pbr).
314 Those fields (vars(self.metadata)) take precedence to
315 supplied attrs.
316 """
317 for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():
318 vars(self.metadata).setdefault(option, attrs.get(option, default()))
320 @staticmethod
321 def _normalize_version(version):
322 from . import sic
324 if isinstance(version, numbers.Number):
325 # Some people apparently take "version number" too literally :)
326 version = str(version)
327 elif isinstance(version, sic) or version is None:
328 return version
330 normalized = str(Version(version))
331 if version != normalized:
332 InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")
333 return normalized
334 return version
336 def _finalize_requires(self):
337 """
338 Set `metadata.python_requires` and fix environment markers
339 in `install_requires` and `extras_require`.
340 """
341 if getattr(self, 'python_requires', None):
342 self.metadata.python_requires = self.python_requires
344 self._normalize_requires()
345 self.metadata.install_requires = self.install_requires
346 self.metadata.extras_require = self.extras_require
348 if self.extras_require:
349 for extra in self.extras_require.keys():
350 # Setuptools allows a weird "<name>:<env markers> syntax for extras
351 extra = extra.split(':')[0]
352 if extra:
353 self.metadata.provides_extras.setdefault(extra)
355 def _normalize_requires(self):
356 """Make sure requirement-related attributes exist and are normalized"""
357 install_requires = getattr(self, "install_requires", None) or []
358 extras_require = getattr(self, "extras_require", None) or {}
359 self.install_requires = list(map(str, _reqs.parse(install_requires)))
360 self.extras_require = {
361 k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items()
362 }
364 def _finalize_license_files(self) -> None:
365 """Compute names of all license files which should be included."""
366 license_files: list[str] | None = self.metadata.license_files
367 patterns: list[str] = license_files if license_files else []
369 license_file: str | None = self.metadata.license_file
370 if license_file and license_file not in patterns:
371 patterns.append(license_file)
373 if license_files is None and license_file is None:
374 # Default patterns match the ones wheel uses
375 # See https://wheel.readthedocs.io/en/stable/user_guide.html
376 # -> 'Including license files in the generated wheel file'
377 patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']
379 self.metadata.license_files = list(
380 unique_everseen(self._expand_patterns(patterns))
381 )
383 @staticmethod
384 def _expand_patterns(patterns):
385 """
386 >>> list(Distribution._expand_patterns(['LICENSE']))
387 ['LICENSE']
388 >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))
389 ['pyproject.toml', 'LICENSE']
390 """
391 return (
392 path
393 for pattern in patterns
394 for path in sorted(iglob(pattern))
395 if not path.endswith('~') and os.path.isfile(path)
396 )
398 # FIXME: 'Distribution._parse_config_files' is too complex (14)
399 def _parse_config_files(self, filenames=None): # noqa: C901
400 """
401 Adapted from distutils.dist.Distribution.parse_config_files,
402 this method provides the same functionality in subtly-improved
403 ways.
404 """
405 from configparser import ConfigParser
407 # Ignore install directory options if we have a venv
408 ignore_options = (
409 []
410 if sys.prefix == sys.base_prefix
411 else [
412 'install-base',
413 'install-platbase',
414 'install-lib',
415 'install-platlib',
416 'install-purelib',
417 'install-headers',
418 'install-scripts',
419 'install-data',
420 'prefix',
421 'exec-prefix',
422 'home',
423 'user',
424 'root',
425 ]
426 )
428 ignore_options = frozenset(ignore_options)
430 if filenames is None:
431 filenames = self.find_config_files()
433 if DEBUG:
434 self.announce("Distribution.parse_config_files():")
436 parser = ConfigParser()
437 parser.optionxform = str
438 for filename in filenames:
439 with open(filename, encoding='utf-8') as reader:
440 if DEBUG:
441 self.announce(" reading {filename}".format(**locals()))
442 parser.read_file(reader)
443 for section in parser.sections():
444 options = parser.options(section)
445 opt_dict = self.get_option_dict(section)
447 for opt in options:
448 if opt == '__name__' or opt in ignore_options:
449 continue
451 val = parser.get(section, opt)
452 opt = self.warn_dash_deprecation(opt, section)
453 opt = self.make_option_lowercase(opt, section)
454 opt_dict[opt] = (filename, val)
456 # Make the ConfigParser forget everything (so we retain
457 # the original filenames that options come from)
458 parser.__init__()
460 if 'global' not in self.command_options:
461 return
463 # If there was a "global" section in the config file, use it
464 # to set Distribution options.
466 for opt, (src, val) in self.command_options['global'].items():
467 alias = self.negative_opt.get(opt)
468 if alias:
469 val = not strtobool(val)
470 elif opt in ('verbose', 'dry_run'): # ugh!
471 val = strtobool(val)
473 try:
474 setattr(self, alias or opt, val)
475 except ValueError as e:
476 raise DistutilsOptionError(e) from e
478 def warn_dash_deprecation(self, opt, section):
479 if section in (
480 'options.extras_require',
481 'options.data_files',
482 ):
483 return opt
485 underscore_opt = opt.replace('-', '_')
486 commands = list(
487 itertools.chain(
488 distutils.command.__all__,
489 self._setuptools_commands(),
490 )
491 )
492 if (
493 not section.startswith('options')
494 and section != 'metadata'
495 and section not in commands
496 ):
497 return underscore_opt
499 if '-' in opt:
500 SetuptoolsDeprecationWarning.emit(
501 "Invalid dash-separated options",
502 f"""
503 Usage of dash-separated {opt!r} will not be supported in future
504 versions. Please use the underscore name {underscore_opt!r} instead.
505 """,
506 see_docs="userguide/declarative_config.html",
507 due_date=(2024, 9, 26),
508 # Warning initially introduced in 3 Mar 2021
509 )
510 return underscore_opt
512 def _setuptools_commands(self):
513 try:
514 entry_points = metadata.distribution('setuptools').entry_points
515 return {ep.name for ep in entry_points} # Avoid newer API for compatibility
516 except metadata.PackageNotFoundError:
517 # during bootstrapping, distribution doesn't exist
518 return []
520 def make_option_lowercase(self, opt, section):
521 if section != 'metadata' or opt.islower():
522 return opt
524 lowercase_opt = opt.lower()
525 SetuptoolsDeprecationWarning.emit(
526 "Invalid uppercase configuration",
527 f"""
528 Usage of uppercase key {opt!r} in {section!r} will not be supported in
529 future versions. Please use lowercase {lowercase_opt!r} instead.
530 """,
531 see_docs="userguide/declarative_config.html",
532 due_date=(2024, 9, 26),
533 # Warning initially introduced in 6 Mar 2021
534 )
535 return lowercase_opt
537 # FIXME: 'Distribution._set_command_options' is too complex (14)
538 def _set_command_options(self, command_obj, option_dict=None): # noqa: C901
539 """
540 Set the options for 'command_obj' from 'option_dict'. Basically
541 this means copying elements of a dictionary ('option_dict') to
542 attributes of an instance ('command').
544 'command_obj' must be a Command instance. If 'option_dict' is not
545 supplied, uses the standard option dictionary for this command
546 (from 'self.command_options').
548 (Adopted from distutils.dist.Distribution._set_command_options)
549 """
550 command_name = command_obj.get_command_name()
551 if option_dict is None:
552 option_dict = self.get_option_dict(command_name)
554 if DEBUG:
555 self.announce(" setting options for '%s' command:" % command_name)
556 for option, (source, value) in option_dict.items():
557 if DEBUG:
558 self.announce(" %s = %s (from %s)" % (option, value, source))
559 try:
560 bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]
561 except AttributeError:
562 bool_opts = []
563 try:
564 neg_opt = command_obj.negative_opt
565 except AttributeError:
566 neg_opt = {}
568 try:
569 is_string = isinstance(value, str)
570 if option in neg_opt and is_string:
571 setattr(command_obj, neg_opt[option], not strtobool(value))
572 elif option in bool_opts and is_string:
573 setattr(command_obj, option, strtobool(value))
574 elif hasattr(command_obj, option):
575 setattr(command_obj, option, value)
576 else:
577 raise DistutilsOptionError(
578 "error in %s: command '%s' has no such option '%s'"
579 % (source, command_name, option)
580 )
581 except ValueError as e:
582 raise DistutilsOptionError(e) from e
584 def _get_project_config_files(self, filenames):
585 """Add default file and split between INI and TOML"""
586 tomlfiles = []
587 standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")
588 if filenames is not None:
589 parts = partition(lambda f: Path(f).suffix == ".toml", filenames)
590 filenames = list(parts[0]) # 1st element => predicate is False
591 tomlfiles = list(parts[1]) # 2nd element => predicate is True
592 elif standard_project_metadata.exists():
593 tomlfiles = [standard_project_metadata]
594 return filenames, tomlfiles
596 def parse_config_files(self, filenames=None, ignore_option_errors=False):
597 """Parses configuration files from various levels
598 and loads configuration.
599 """
600 inifiles, tomlfiles = self._get_project_config_files(filenames)
602 self._parse_config_files(filenames=inifiles)
604 setupcfg.parse_configuration(
605 self, self.command_options, ignore_option_errors=ignore_option_errors
606 )
607 for filename in tomlfiles:
608 pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
610 self._finalize_requires()
611 self._finalize_license_files()
613 def fetch_build_eggs(self, requires):
614 """Resolve pre-setup requirements"""
615 from .installer import _fetch_build_eggs
617 return _fetch_build_eggs(self, requires)
619 def finalize_options(self):
620 """
621 Allow plugins to apply arbitrary operations to the
622 distribution. Each hook may optionally define a 'order'
623 to influence the order of execution. Smaller numbers
624 go first and the default is 0.
625 """
626 group = 'setuptools.finalize_distribution_options'
628 def by_order(hook):
629 return getattr(hook, 'order', 0)
631 defined = metadata.entry_points(group=group)
632 filtered = itertools.filterfalse(self._removed, defined)
633 loaded = map(lambda e: e.load(), filtered)
634 for ep in sorted(loaded, key=by_order):
635 ep(self)
637 @staticmethod
638 def _removed(ep):
639 """
640 When removing an entry point, if metadata is loaded
641 from an older version of Setuptools, that removed
642 entry point will attempt to be loaded and will fail.
643 See #2765 for more details.
644 """
645 removed = {
646 # removed 2021-09-05
647 '2to3_doctests',
648 }
649 return ep.name in removed
651 def _finalize_setup_keywords(self):
652 for ep in metadata.entry_points(group='distutils.setup_keywords'):
653 value = getattr(self, ep.name, None)
654 if value is not None:
655 ep.load()(self, ep.name, value)
657 def get_egg_cache_dir(self):
658 from . import windows_support
660 egg_cache_dir = os.path.join(os.curdir, '.eggs')
661 if not os.path.exists(egg_cache_dir):
662 os.mkdir(egg_cache_dir)
663 windows_support.hide_file(egg_cache_dir)
664 readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')
665 with open(readme_txt_filename, 'w', encoding="utf-8") as f:
666 f.write(
667 'This directory contains eggs that were downloaded '
668 'by setuptools to build, test, and run plug-ins.\n\n'
669 )
670 f.write(
671 'This directory caches those eggs to prevent '
672 'repeated downloads.\n\n'
673 )
674 f.write('However, it is safe to delete this directory.\n\n')
676 return egg_cache_dir
678 def fetch_build_egg(self, req):
679 """Fetch an egg needed for building"""
680 from .installer import fetch_build_egg
682 return fetch_build_egg(self, req)
684 def get_command_class(self, command):
685 """Pluggable version of get_command_class()"""
686 if command in self.cmdclass:
687 return self.cmdclass[command]
689 # Special case bdist_wheel so it's never loaded from "wheel"
690 if command == 'bdist_wheel':
691 from .command.bdist_wheel import bdist_wheel
693 return bdist_wheel
695 eps = metadata.entry_points(group='distutils.commands', name=command)
696 for ep in eps:
697 self.cmdclass[command] = cmdclass = ep.load()
698 return cmdclass
699 else:
700 return _Distribution.get_command_class(self, command)
702 def print_commands(self):
703 for ep in metadata.entry_points(group='distutils.commands'):
704 if ep.name not in self.cmdclass:
705 cmdclass = ep.load()
706 self.cmdclass[ep.name] = cmdclass
707 return _Distribution.print_commands(self)
709 def get_command_list(self):
710 for ep in metadata.entry_points(group='distutils.commands'):
711 if ep.name not in self.cmdclass:
712 cmdclass = ep.load()
713 self.cmdclass[ep.name] = cmdclass
714 return _Distribution.get_command_list(self)
716 def include(self, **attrs):
717 """Add items to distribution that are named in keyword arguments
719 For example, 'dist.include(py_modules=["x"])' would add 'x' to
720 the distribution's 'py_modules' attribute, if it was not already
721 there.
723 Currently, this method only supports inclusion for attributes that are
724 lists or tuples. If you need to add support for adding to other
725 attributes in this or a subclass, you can add an '_include_X' method,
726 where 'X' is the name of the attribute. The method will be called with
727 the value passed to 'include()'. So, 'dist.include(foo={"bar":"baz"})'
728 will try to call 'dist._include_foo({"bar":"baz"})', which can then
729 handle whatever special inclusion logic is needed.
730 """
731 for k, v in attrs.items():
732 include = getattr(self, '_include_' + k, None)
733 if include:
734 include(v)
735 else:
736 self._include_misc(k, v)
738 def exclude_package(self, package):
739 """Remove packages, modules, and extensions in named package"""
741 pfx = package + '.'
742 if self.packages:
743 self.packages = [
744 p for p in self.packages if p != package and not p.startswith(pfx)
745 ]
747 if self.py_modules:
748 self.py_modules = [
749 p for p in self.py_modules if p != package and not p.startswith(pfx)
750 ]
752 if self.ext_modules:
753 self.ext_modules = [
754 p
755 for p in self.ext_modules
756 if p.name != package and not p.name.startswith(pfx)
757 ]
759 def has_contents_for(self, package):
760 """Return true if 'exclude_package(package)' would do something"""
762 pfx = package + '.'
764 for p in self.iter_distribution_names():
765 if p == package or p.startswith(pfx):
766 return True
768 return False
770 def _exclude_misc(self, name, value):
771 """Handle 'exclude()' for list/tuple attrs without a special handler"""
772 if not isinstance(value, sequence):
773 raise DistutilsSetupError(
774 "%s: setting must be a list or tuple (%r)" % (name, value)
775 )
776 try:
777 old = getattr(self, name)
778 except AttributeError as e:
779 raise DistutilsSetupError("%s: No such distribution setting" % name) from e
780 if old is not None and not isinstance(old, sequence):
781 raise DistutilsSetupError(
782 name + ": this setting cannot be changed via include/exclude"
783 )
784 elif old:
785 setattr(self, name, [item for item in old if item not in value])
787 def _include_misc(self, name, value):
788 """Handle 'include()' for list/tuple attrs without a special handler"""
790 if not isinstance(value, sequence):
791 raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value))
792 try:
793 old = getattr(self, name)
794 except AttributeError as e:
795 raise DistutilsSetupError("%s: No such distribution setting" % name) from e
796 if old is None:
797 setattr(self, name, value)
798 elif not isinstance(old, sequence):
799 raise DistutilsSetupError(
800 name + ": this setting cannot be changed via include/exclude"
801 )
802 else:
803 new = [item for item in value if item not in old]
804 setattr(self, name, old + new)
806 def exclude(self, **attrs):
807 """Remove items from distribution that are named in keyword arguments
809 For example, 'dist.exclude(py_modules=["x"])' would remove 'x' from
810 the distribution's 'py_modules' attribute. Excluding packages uses
811 the 'exclude_package()' method, so all of the package's contained
812 packages, modules, and extensions are also excluded.
814 Currently, this method only supports exclusion from attributes that are
815 lists or tuples. If you need to add support for excluding from other
816 attributes in this or a subclass, you can add an '_exclude_X' method,
817 where 'X' is the name of the attribute. The method will be called with
818 the value passed to 'exclude()'. So, 'dist.exclude(foo={"bar":"baz"})'
819 will try to call 'dist._exclude_foo({"bar":"baz"})', which can then
820 handle whatever special exclusion logic is needed.
821 """
822 for k, v in attrs.items():
823 exclude = getattr(self, '_exclude_' + k, None)
824 if exclude:
825 exclude(v)
826 else:
827 self._exclude_misc(k, v)
829 def _exclude_packages(self, packages):
830 if not isinstance(packages, sequence):
831 raise DistutilsSetupError(
832 "packages: setting must be a list or tuple (%r)" % (packages,)
833 )
834 list(map(self.exclude_package, packages))
836 def _parse_command_opts(self, parser, args):
837 # Remove --with-X/--without-X options when processing command args
838 self.global_options = self.__class__.global_options
839 self.negative_opt = self.__class__.negative_opt
841 # First, expand any aliases
842 command = args[0]
843 aliases = self.get_option_dict('aliases')
844 while command in aliases:
845 src, alias = aliases[command]
846 del aliases[command] # ensure each alias can expand only once!
847 import shlex
849 args[:1] = shlex.split(alias, True)
850 command = args[0]
852 nargs = _Distribution._parse_command_opts(self, parser, args)
854 # Handle commands that want to consume all remaining arguments
855 cmd_class = self.get_command_class(command)
856 if getattr(cmd_class, 'command_consumes_arguments', None):
857 self.get_option_dict(command)['args'] = ("command line", nargs)
858 if nargs is not None:
859 return []
861 return nargs
863 def get_cmdline_options(self):
864 """Return a '{cmd: {opt:val}}' map of all command-line options
866 Option names are all long, but do not include the leading '--', and
867 contain dashes rather than underscores. If the option doesn't take
868 an argument (e.g. '--quiet'), the 'val' is 'None'.
870 Note that options provided by config files are intentionally excluded.
871 """
873 d = {}
875 for cmd, opts in self.command_options.items():
876 for opt, (src, val) in opts.items():
877 if src != "command line":
878 continue
880 opt = opt.replace('_', '-')
882 if val == 0:
883 cmdobj = self.get_command_obj(cmd)
884 neg_opt = self.negative_opt.copy()
885 neg_opt.update(getattr(cmdobj, 'negative_opt', {}))
886 for neg, pos in neg_opt.items():
887 if pos == opt:
888 opt = neg
889 val = None
890 break
891 else:
892 raise AssertionError("Shouldn't be able to get here")
894 elif val == 1:
895 val = None
897 d.setdefault(cmd, {})[opt] = val
899 return d
901 def iter_distribution_names(self):
902 """Yield all packages, modules, and extension names in distribution"""
904 yield from self.packages or ()
906 yield from self.py_modules or ()
908 for ext in self.ext_modules or ():
909 if isinstance(ext, tuple):
910 name, buildinfo = ext
911 else:
912 name = ext.name
913 if name.endswith('module'):
914 name = name[:-6]
915 yield name
917 def handle_display_options(self, option_order):
918 """If there were any non-global "display-only" options
919 (--help-commands or the metadata display options) on the command
920 line, display the requested info and return true; else return
921 false.
922 """
923 import sys
925 if self.help_commands:
926 return _Distribution.handle_display_options(self, option_order)
928 # Stdout may be StringIO (e.g. in tests)
929 if not isinstance(sys.stdout, io.TextIOWrapper):
930 return _Distribution.handle_display_options(self, option_order)
932 # Don't wrap stdout if utf-8 is already the encoding. Provides
933 # workaround for #334.
934 if sys.stdout.encoding.lower() in ('utf-8', 'utf8'):
935 return _Distribution.handle_display_options(self, option_order)
937 # Print metadata in UTF-8 no matter the platform
938 encoding = sys.stdout.encoding
939 sys.stdout.reconfigure(encoding='utf-8')
940 try:
941 return _Distribution.handle_display_options(self, option_order)
942 finally:
943 sys.stdout.reconfigure(encoding=encoding)
945 def run_command(self, command):
946 self.set_defaults()
947 # Postpone defaults until all explicit configuration is considered
948 # (setup() args, config files, command line and plugins)
950 super().run_command(command)
953class DistDeprecationWarning(SetuptoolsDeprecationWarning):
954 """Class for warning about deprecations in dist in
955 setuptools. Not ignored by default, unlike DeprecationWarning."""