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