summaryrefslogtreecommitdiffstats
path: root/third_party/python/setuptools/setuptools/dist.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/setuptools/setuptools/dist.py')
-rw-r--r--third_party/python/setuptools/setuptools/dist.py726
1 files changed, 478 insertions, 248 deletions
diff --git a/third_party/python/setuptools/setuptools/dist.py b/third_party/python/setuptools/setuptools/dist.py
index 2c088ef8cb..4458a58033 100644
--- a/third_party/python/setuptools/setuptools/dist.py
+++ b/third_party/python/setuptools/setuptools/dist.py
@@ -1,127 +1,164 @@
-# -*- coding: utf-8 -*-
__all__ = ['Distribution']
import io
import sys
import re
import os
-import warnings
import numbers
import distutils.log
import distutils.core
import distutils.cmd
import distutils.dist
+import distutils.command
from distutils.util import strtobool
from distutils.debug import DEBUG
from distutils.fancy_getopt import translate_longopt
+from glob import iglob
import itertools
+import textwrap
+from contextlib import suppress
+from typing import List, Optional, Set, TYPE_CHECKING
+from pathlib import Path
from collections import defaultdict
from email import message_from_file
from distutils.errors import DistutilsOptionError, DistutilsSetupError
from distutils.util import rfc822_escape
-from distutils.version import StrictVersion
from setuptools.extern import packaging
from setuptools.extern import ordered_set
-
-from . import SetuptoolsDeprecationWarning
+from setuptools.extern.more_itertools import unique_everseen, partition
import setuptools
+import setuptools.command
from setuptools import windows_support
from setuptools.monkey import get_unpatched
-from setuptools.config import parse_configuration
-import pkg_resources
+from setuptools.config import setupcfg, pyprojecttoml
+from setuptools.discovery import ConfigDiscovery
-__import__('setuptools.extern.packaging.specifiers')
-__import__('setuptools.extern.packaging.version')
+from setuptools.extern.packaging import version
+from . import _reqs
+from . import _entry_points
+from . import _normalization
+from ._importlib import metadata
+from .warnings import InformationOnly, SetuptoolsDeprecationWarning
+if TYPE_CHECKING:
+ from email.message import Message
-def _get_unpatched(cls):
- warnings.warn("Do not call this function", DistDeprecationWarning)
- return get_unpatched(cls)
+__import__('setuptools.extern.packaging.specifiers')
+__import__('setuptools.extern.packaging.version')
def get_metadata_version(self):
mv = getattr(self, 'metadata_version', None)
-
if mv is None:
- if self.long_description_content_type or self.provides_extras:
- mv = StrictVersion('2.1')
- elif (self.maintainer is not None or
- self.maintainer_email is not None or
- getattr(self, 'python_requires', None) is not None or
- self.project_urls):
- mv = StrictVersion('1.2')
- elif (self.provides or self.requires or self.obsoletes or
- self.classifiers or self.download_url):
- mv = StrictVersion('1.1')
- else:
- mv = StrictVersion('1.0')
-
+ mv = version.Version('2.1')
self.metadata_version = mv
-
return mv
-def read_pkg_file(self, file):
- """Reads the metadata values from a file object."""
- msg = message_from_file(file)
+def rfc822_unescape(content: str) -> str:
+ """Reverse RFC-822 escaping by removing leading whitespaces from content."""
+ lines = content.splitlines()
+ if len(lines) == 1:
+ return lines[0].lstrip()
+ return '\n'.join((lines[0].lstrip(), textwrap.dedent('\n'.join(lines[1:]))))
+
+
+def _read_field_from_msg(msg: "Message", field: str) -> Optional[str]:
+ """Read Message header field."""
+ value = msg[field]
+ if value == 'UNKNOWN':
+ return None
+ return value
+
- def _read_field(name):
- value = msg[name]
- if value == 'UNKNOWN':
- return None
+def _read_field_unescaped_from_msg(msg: "Message", field: str) -> Optional[str]:
+ """Read Message header field and apply rfc822_unescape."""
+ value = _read_field_from_msg(msg, field)
+ if value is None:
return value
+ return rfc822_unescape(value)
- def _read_list(name):
- values = msg.get_all(name, None)
- if values == []:
- return None
- return values
- self.metadata_version = StrictVersion(msg['metadata-version'])
- self.name = _read_field('name')
- self.version = _read_field('version')
- self.description = _read_field('summary')
+def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]:
+ """Read Message header field and return all results as list."""
+ values = msg.get_all(field, None)
+ if values == []:
+ return None
+ return values
+
+
+def _read_payload_from_msg(msg: "Message") -> Optional[str]:
+ value = msg.get_payload().strip()
+ if value == 'UNKNOWN' or not value:
+ return None
+ return value
+
+
+def read_pkg_file(self, file):
+ """Reads the metadata values from a file object."""
+ msg = message_from_file(file)
+
+ self.metadata_version = version.Version(msg['metadata-version'])
+ self.name = _read_field_from_msg(msg, 'name')
+ self.version = _read_field_from_msg(msg, 'version')
+ self.description = _read_field_from_msg(msg, 'summary')
# we are filling author only.
- self.author = _read_field('author')
+ self.author = _read_field_from_msg(msg, 'author')
self.maintainer = None
- self.author_email = _read_field('author-email')
+ self.author_email = _read_field_from_msg(msg, 'author-email')
self.maintainer_email = None
- self.url = _read_field('home-page')
- self.license = _read_field('license')
-
- if 'download-url' in msg:
- self.download_url = _read_field('download-url')
- else:
- self.download_url = None
-
- self.long_description = _read_field('description')
- self.description = _read_field('summary')
+ self.url = _read_field_from_msg(msg, 'home-page')
+ self.download_url = _read_field_from_msg(msg, 'download-url')
+ self.license = _read_field_unescaped_from_msg(msg, 'license')
+
+ self.long_description = _read_field_unescaped_from_msg(msg, 'description')
+ if (
+ self.long_description is None and
+ self.metadata_version >= version.Version('2.1')
+ ):
+ self.long_description = _read_payload_from_msg(msg)
+ self.description = _read_field_from_msg(msg, 'summary')
if 'keywords' in msg:
- self.keywords = _read_field('keywords').split(',')
+ self.keywords = _read_field_from_msg(msg, 'keywords').split(',')
- self.platforms = _read_list('platform')
- self.classifiers = _read_list('classifier')
+ self.platforms = _read_list_from_msg(msg, 'platform')
+ self.classifiers = _read_list_from_msg(msg, 'classifier')
# PEP 314 - these fields only exist in 1.1
- if self.metadata_version == StrictVersion('1.1'):
- self.requires = _read_list('requires')
- self.provides = _read_list('provides')
- self.obsoletes = _read_list('obsoletes')
+ if self.metadata_version == version.Version('1.1'):
+ self.requires = _read_list_from_msg(msg, 'requires')
+ self.provides = _read_list_from_msg(msg, 'provides')
+ self.obsoletes = _read_list_from_msg(msg, 'obsoletes')
else:
self.requires = None
self.provides = None
self.obsoletes = None
+ self.license_files = _read_list_from_msg(msg, 'license-file')
-# Based on Python 3.5 version
-def write_pkg_file(self, file):
- """Write the PKG-INFO format data to a file object.
+
+def single_line(val):
+ """
+ Quick and dirty validation for Summary pypa/setuptools#1390.
"""
+ if '\n' in val:
+ # TODO: Replace with `raise ValueError("newlines not allowed")`
+ # after reviewing #2893.
+ msg = "newlines are not allowed in `summary` and will break in the future"
+ SetuptoolsDeprecationWarning.emit("Invalid config.", msg)
+ # due_date is undefined. Controversial change, there was a lot of push back.
+ val = val.strip().split('\n')[0]
+ return val
+
+
+# Based on Python 3.5 version
+def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
+ """Write the PKG-INFO format data to a file object."""
version = self.get_metadata_version()
def write_field(key, value):
@@ -130,44 +167,39 @@ def write_pkg_file(self, file):
write_field('Metadata-Version', str(version))
write_field('Name', self.get_name())
write_field('Version', self.get_version())
- write_field('Summary', self.get_description())
- write_field('Home-page', self.get_url())
- if version < StrictVersion('1.2'):
- write_field('Author', self.get_contact())
- write_field('Author-email', self.get_contact_email())
- else:
- optional_fields = (
- ('Author', 'author'),
- ('Author-email', 'author_email'),
- ('Maintainer', 'maintainer'),
- ('Maintainer-email', 'maintainer_email'),
- )
+ summary = self.get_description()
+ if summary:
+ write_field('Summary', single_line(summary))
+
+ optional_fields = (
+ ('Home-page', 'url'),
+ ('Download-URL', 'download_url'),
+ ('Author', 'author'),
+ ('Author-email', 'author_email'),
+ ('Maintainer', 'maintainer'),
+ ('Maintainer-email', 'maintainer_email'),
+ )
- for field, attr in optional_fields:
- attr_val = getattr(self, attr)
+ for field, attr in optional_fields:
+ attr_val = getattr(self, attr, None)
+ if attr_val is not None:
+ write_field(field, attr_val)
- if attr_val is not None:
- write_field(field, attr_val)
+ license = self.get_license()
+ if license:
+ write_field('License', rfc822_escape(license))
- write_field('License', self.get_license())
- if self.download_url:
- write_field('Download-URL', self.download_url)
for project_url in self.project_urls.items():
write_field('Project-URL', '%s, %s' % project_url)
- long_desc = rfc822_escape(self.get_long_description())
- write_field('Description', long_desc)
-
keywords = ','.join(self.get_keywords())
if keywords:
write_field('Keywords', keywords)
- if version >= StrictVersion('1.2'):
- for platform in self.get_platforms():
- write_field('Platform', platform)
- else:
- self._write_list(file, 'Platform', self.get_platforms())
+ platforms = self.get_platforms() or []
+ for platform in platforms:
+ write_field('Platform', platform)
self._write_list(file, 'Classifier', self.get_classifiers())
@@ -182,26 +214,30 @@ def write_pkg_file(self, file):
# PEP 566
if self.long_description_content_type:
- write_field(
- 'Description-Content-Type',
- self.long_description_content_type
- )
+ write_field('Description-Content-Type', self.long_description_content_type)
if self.provides_extras:
for extra in self.provides_extras:
write_field('Provides-Extra', extra)
+ self._write_list(file, 'License-File', self.license_files or [])
+
+ long_description = self.get_long_description()
+ if long_description:
+ file.write("\n%s" % long_description)
+ if not long_description.endswith("\n"):
+ file.write("\n")
+
sequence = tuple, list
def check_importable(dist, attr, value):
try:
- ep = pkg_resources.EntryPoint.parse('x=' + value)
+ ep = metadata.EntryPoint(value=value, name=None, group=None)
assert not ep.extras
except (TypeError, ValueError, AttributeError, AssertionError) as e:
raise DistutilsSetupError(
- "%r must be importable 'module:attrs' string (got %r)"
- % (attr, value)
+ "%r must be importable 'module:attrs' string (got %r)" % (attr, value)
) from e
@@ -226,15 +262,26 @@ def check_nsp(dist, attr, value):
for nsp in ns_packages:
if not dist.has_contents_for(nsp):
raise DistutilsSetupError(
- "Distribution contains no modules or packages for " +
- "namespace package %r" % nsp
+ "Distribution contains no modules or packages for "
+ + "namespace package %r" % nsp
)
parent, sep, child = nsp.rpartition('.')
if parent and parent not in ns_packages:
distutils.log.warn(
"WARNING: %r is declared as a package namespace, but %r"
- " is not: please correct this in setup.py", nsp, parent
+ " is not: please correct this in setup.py",
+ nsp,
+ parent,
)
+ SetuptoolsDeprecationWarning.emit(
+ "The namespace_packages parameter is deprecated.",
+ "Please replace its usage with implicit namespaces (PEP 420).",
+ see_docs="references/keywords.html#keyword-namespace-packages"
+ # TODO: define due_date, it may break old packages that are no longer
+ # maintained (e.g. sphinxcontrib extensions) when installed from source.
+ # Warning officially introduced in May 2022, however the deprecation
+ # was mentioned much earlier in the docs (May 2020, see #2149).
+ )
def check_extras(dist, attr, value):
@@ -251,9 +298,19 @@ def check_extras(dist, attr, value):
def _check_extra(extra, reqs):
name, sep, marker = extra.partition(':')
- if marker and pkg_resources.invalid_marker(marker):
- raise DistutilsSetupError("Invalid environment marker: " + marker)
- list(pkg_resources.parse_requirements(reqs))
+ try:
+ _check_marker(marker)
+ except packaging.markers.InvalidMarker:
+ msg = f"Invalid environment marker: {marker} ({extra!r})"
+ raise DistutilsSetupError(msg) from None
+ list(_reqs.parse(reqs))
+
+
+def _check_marker(marker):
+ if not marker:
+ return
+ m = packaging.markers.Marker(marker)
+ m.evaluate()
def assert_bool(dist, attr, value):
@@ -263,10 +320,18 @@ def assert_bool(dist, attr, value):
raise DistutilsSetupError(tmpl.format(attr=attr, value=value))
+def invalid_unless_false(dist, attr, value):
+ if not value:
+ DistDeprecationWarning.emit(f"{attr} is ignored.")
+ # TODO: should there be a `due_date` here?
+ return
+ raise DistutilsSetupError(f"{attr} is invalid.")
+
+
def check_requirements(dist, attr, value):
"""Verify that install_requires is a valid requirements list"""
try:
- list(pkg_resources.parse_requirements(value))
+ list(_reqs.parse(value))
if isinstance(value, (dict, set)):
raise TypeError("Unordered types are not allowed")
except (TypeError, ValueError) as error:
@@ -274,30 +339,25 @@ def check_requirements(dist, attr, value):
"{attr!r} must be a string or list of strings "
"containing valid project/version requirement specifiers; {error}"
)
- raise DistutilsSetupError(
- tmpl.format(attr=attr, error=error)
- ) from error
+ raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
def check_specifier(dist, attr, value):
"""Verify that value is a valid version specifier"""
try:
packaging.specifiers.SpecifierSet(value)
- except packaging.specifiers.InvalidSpecifier as error:
+ except (packaging.specifiers.InvalidSpecifier, AttributeError) as error:
tmpl = (
- "{attr!r} must be a string "
- "containing valid version specifiers; {error}"
+ "{attr!r} must be a string " "containing valid version specifiers; {error}"
)
- raise DistutilsSetupError(
- tmpl.format(attr=attr, error=error)
- ) from error
+ raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error
def check_entry_points(dist, attr, value):
"""Verify that entry_points map is parseable"""
try:
- pkg_resources.EntryPoint.parse_map(value)
- except ValueError as e:
+ _entry_points.load(value)
+ except Exception as e:
raise DistutilsSetupError(e) from e
@@ -311,12 +371,12 @@ def check_package_data(dist, attr, value):
if not isinstance(value, dict):
raise DistutilsSetupError(
"{!r} must be a dictionary mapping package names to lists of "
- "string wildcard patterns".format(attr))
+ "string wildcard patterns".format(attr)
+ )
for k, v in value.items():
if not isinstance(k, str):
raise DistutilsSetupError(
- "keys of {!r} dict must be strings (got {!r})"
- .format(attr, k)
+ "keys of {!r} dict must be strings (got {!r})".format(attr, k)
)
assert_string_list(dist, 'values of {!r} dict'.format(attr), v)
@@ -326,7 +386,8 @@ def check_packages(dist, attr, value):
if not re.match(r'\w+(\.\w+)*', pkgname):
distutils.log.warn(
"WARNING: %r not a valid package name; please use only "
- ".-separated package names in setup.py", pkgname
+ ".-separated package names in setup.py",
+ pkgname,
)
@@ -386,10 +447,11 @@ class Distribution(_Distribution):
"""
_DISTUTILS_UNSUPPORTED_METADATA = {
- 'long_description_content_type': None,
+ 'long_description_content_type': lambda: None,
'project_urls': dict,
'provides_extras': ordered_set.OrderedSet,
- 'license_files': ordered_set.OrderedSet,
+ 'license_file': lambda: None,
+ 'license_files': lambda: None,
}
_patched_dist = None
@@ -401,11 +463,12 @@ class Distribution(_Distribution):
#
if not attrs or 'name' not in attrs or 'version' not in attrs:
return
- key = pkg_resources.safe_name(str(attrs['name'])).lower()
- dist = pkg_resources.working_set.by_key.get(key)
- if dist is not None and not dist.has_metadata('PKG-INFO'):
- dist._version = pkg_resources.safe_version(str(attrs['version']))
- self._patched_dist = dist
+ name = _normalization.safe_name(str(attrs['name'])).lower()
+ with suppress(metadata.PackageNotFoundError):
+ dist = metadata.distribution(name)
+ if dist is not None and not dist.read_text('PKG-INFO'):
+ dist._version = _normalization.safe_version(str(attrs['version']))
+ self._patched_dist = dist
def __init__(self, attrs=None):
have_package_data = hasattr(self, "package_data")
@@ -418,29 +481,59 @@ class Distribution(_Distribution):
self.patch_missing_pkg_info(attrs)
self.dependency_links = attrs.pop('dependency_links', [])
self.setup_requires = attrs.pop('setup_requires', [])
- for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
+ for ep in metadata.entry_points(group='distutils.setup_keywords'):
vars(self).setdefault(ep.name, None)
- _Distribution.__init__(self, {
- k: v for k, v in attrs.items()
- if k not in self._DISTUTILS_UNSUPPORTED_METADATA
- })
-
- # Fill-in missing metadata fields not supported by distutils.
- # Note some fields may have been set by other tools (e.g. pbr)
- # above; they are taken preferrentially to setup() arguments
- for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():
- for source in self.metadata.__dict__, attrs:
- if option in source:
- value = source[option]
- break
- else:
- value = default() if default else None
- setattr(self.metadata, option, value)
+ _Distribution.__init__(
+ self,
+ {
+ k: v
+ for k, v in attrs.items()
+ if k not in self._DISTUTILS_UNSUPPORTED_METADATA
+ },
+ )
+
+ # Private API (setuptools-use only, not restricted to Distribution)
+ # Stores files that are referenced by the configuration and need to be in the
+ # sdist (e.g. `version = file: VERSION.txt`)
+ self._referenced_files: Set[str] = set()
+
+ # Save the original dependencies before they are processed into the egg format
+ self._orig_extras_require = {}
+ self._orig_install_requires = []
+ self._tmp_extras_require = defaultdict(ordered_set.OrderedSet)
+
+ self.set_defaults = ConfigDiscovery(self)
+
+ self._set_metadata_defaults(attrs)
self.metadata.version = self._normalize_version(
- self._validate_version(self.metadata.version))
+ self._validate_version(self.metadata.version)
+ )
self._finalize_requires()
+ def _validate_metadata(self):
+ required = {"name"}
+ provided = {
+ key
+ for key in vars(self.metadata)
+ if getattr(self.metadata, key, None) is not None
+ }
+ missing = required - provided
+
+ if missing:
+ msg = f"Required package metadata is missing: {missing}"
+ raise DistutilsSetupError(msg)
+
+ def _set_metadata_defaults(self, attrs):
+ """
+ Fill-in missing metadata fields not supported by distutils.
+ Some fields may have been set by other tools (e.g. pbr).
+ Those fields (vars(self.metadata)) take precedence to
+ supplied attrs.
+ """
+ for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items():
+ vars(self.metadata).setdefault(option, attrs.get(option, default()))
+
@staticmethod
def _normalize_version(version):
if isinstance(version, setuptools.sic) or version is None:
@@ -448,8 +541,7 @@ class Distribution(_Distribution):
normalized = str(packaging.version.Version(version))
if version != normalized:
- tmpl = "Normalizing '{version}' to '{normalized}'"
- warnings.warn(tmpl.format(**locals()))
+ InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'")
return normalized
return version
@@ -463,11 +555,17 @@ class Distribution(_Distribution):
try:
packaging.version.Version(version)
except (packaging.version.InvalidVersion, TypeError):
- warnings.warn(
- "The version specified (%r) is an invalid version, this "
- "may not work as expected with newer versions of "
- "setuptools, pip, and PyPI. Please see PEP 440 for more "
- "details." % version
+ SetuptoolsDeprecationWarning.emit(
+ f"Invalid version: {version!r}.",
+ """
+ The version specified is not a valid version according to PEP 440.
+ This may not work as expected with newer versions of
+ setuptools, pip, and PyPI.
+ """,
+ see_url="https://peps.python.org/pep-0440/",
+ due_date=(2023, 9, 26),
+ # Warning initially introduced in 26 Sept 2014
+ # pypa/packaging already removed legacy versions.
)
return setuptools.sic(version)
return version
@@ -481,6 +579,8 @@ class Distribution(_Distribution):
self.metadata.python_requires = self.python_requires
if getattr(self, 'extras_require', None):
+ # Save original before it is messed by _convert_extras_requirements
+ self._orig_extras_require = self._orig_extras_require or self.extras_require
for extra in self.extras_require.keys():
# Since this gets called multiple times at points where the
# keys have become 'converted' extras, ensure that we are only
@@ -489,6 +589,10 @@ class Distribution(_Distribution):
if extra:
self.metadata.provides_extras.add(extra)
+ if getattr(self, 'install_requires', None) and not self._orig_install_requires:
+ # Save original before it is messed by _move_install_requirements_markers
+ self._orig_install_requires = self.install_requires
+
self._convert_extras_requirements()
self._move_install_requirements_markers()
@@ -499,11 +603,12 @@ class Distribution(_Distribution):
`"extra:{marker}": ["barbazquux"]`.
"""
spec_ext_reqs = getattr(self, 'extras_require', None) or {}
- self._tmp_extras_require = defaultdict(list)
+ tmp = defaultdict(ordered_set.OrderedSet)
+ self._tmp_extras_require = getattr(self, '_tmp_extras_require', tmp)
for section, v in spec_ext_reqs.items():
# Do not strip empty sections.
self._tmp_extras_require[section]
- for r in pkg_resources.parse_requirements(v):
+ for r in _reqs.parse(v):
suffix = self._suffix_for(r)
self._tmp_extras_require[section + suffix].append(r)
@@ -529,7 +634,7 @@ class Distribution(_Distribution):
return not req.marker
spec_inst_reqs = getattr(self, 'install_requires', None) or ()
- inst_reqs = list(pkg_resources.parse_requirements(spec_inst_reqs))
+ inst_reqs = list(_reqs.parse(spec_inst_reqs))
simple_reqs = filter(is_simple_req, inst_reqs)
complex_reqs = itertools.filterfalse(is_simple_req, inst_reqs)
self.install_requires = list(map(str, simple_reqs))
@@ -537,7 +642,8 @@ class Distribution(_Distribution):
for r in complex_reqs:
self._tmp_extras_require[':' + str(r.marker)].append(r)
self.extras_require = dict(
- (k, [str(r) for r in map(self._clean_req, v)])
+ # list(dict.fromkeys(...)) ensures a list of unique strings
+ (k, list(dict.fromkeys(str(r) for r in map(self._clean_req, v))))
for k, v in self._tmp_extras_require.items()
)
@@ -548,7 +654,42 @@ class Distribution(_Distribution):
req.marker = None
return req
- def _parse_config_files(self, filenames=None):
+ def _finalize_license_files(self):
+ """Compute names of all license files which should be included."""
+ license_files: Optional[List[str]] = self.metadata.license_files
+ patterns: List[str] = license_files if license_files else []
+
+ license_file: Optional[str] = self.metadata.license_file
+ if license_file and license_file not in patterns:
+ patterns.append(license_file)
+
+ if license_files is None and license_file is None:
+ # Default patterns match the ones wheel uses
+ # See https://wheel.readthedocs.io/en/stable/user_guide.html
+ # -> 'Including license files in the generated wheel file'
+ patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
+
+ self.metadata.license_files = list(
+ unique_everseen(self._expand_patterns(patterns))
+ )
+
+ @staticmethod
+ def _expand_patterns(patterns):
+ """
+ >>> list(Distribution._expand_patterns(['LICENSE']))
+ ['LICENSE']
+ >>> list(Distribution._expand_patterns(['setup.cfg', 'LIC*']))
+ ['setup.cfg', 'LICENSE']
+ """
+ return (
+ path
+ for pattern in patterns
+ for path in sorted(iglob(pattern))
+ if not path.endswith('~') and os.path.isfile(path)
+ )
+
+ # FIXME: 'Distribution._parse_config_files' is too complex (14)
+ def _parse_config_files(self, filenames=None): # noqa: C901
"""
Adapted from distutils.dist.Distribution.parse_config_files,
this method provides the same functionality in subtly-improved
@@ -557,14 +698,25 @@ class Distribution(_Distribution):
from configparser import ConfigParser
# Ignore install directory options if we have a venv
- if sys.prefix != sys.base_prefix:
- ignore_options = [
- 'install-base', 'install-platbase', 'install-lib',
- 'install-platlib', 'install-purelib', 'install-headers',
- 'install-scripts', 'install-data', 'prefix', 'exec-prefix',
- 'home', 'user', 'root']
- else:
- ignore_options = []
+ ignore_options = (
+ []
+ if sys.prefix == sys.base_prefix
+ else [
+ 'install-base',
+ 'install-platbase',
+ 'install-lib',
+ 'install-platlib',
+ 'install-purelib',
+ 'install-headers',
+ 'install-scripts',
+ 'install-data',
+ 'prefix',
+ 'exec-prefix',
+ 'home',
+ 'user',
+ 'root',
+ ]
+ )
ignore_options = frozenset(ignore_options)
@@ -575,6 +727,7 @@ class Distribution(_Distribution):
self.announce("Distribution.parse_config_files():")
parser = ConfigParser()
+ parser.optionxform = str
for filename in filenames:
with io.open(filename, encoding='utf-8') as reader:
if DEBUG:
@@ -585,32 +738,94 @@ class Distribution(_Distribution):
opt_dict = self.get_option_dict(section)
for opt in options:
- if opt != '__name__' and opt not in ignore_options:
- val = parser.get(section, opt)
- opt = opt.replace('-', '_')
- opt_dict[opt] = (filename, val)
+ if opt == '__name__' or opt in ignore_options:
+ continue
+
+ val = parser.get(section, opt)
+ opt = self.warn_dash_deprecation(opt, section)
+ opt = self.make_option_lowercase(opt, section)
+ opt_dict[opt] = (filename, val)
# Make the ConfigParser forget everything (so we retain
# the original filenames that options come from)
parser.__init__()
+ if 'global' not in self.command_options:
+ return
+
# If there was a "global" section in the config file, use it
# to set Distribution options.
- if 'global' in self.command_options:
- for (opt, (src, val)) in self.command_options['global'].items():
- alias = self.negative_opt.get(opt)
- try:
- if alias:
- setattr(self, alias, not strtobool(val))
- elif opt in ('verbose', 'dry_run'): # ugh!
- setattr(self, opt, strtobool(val))
- else:
- setattr(self, opt, val)
- except ValueError as e:
- raise DistutilsOptionError(e) from e
+ for (opt, (src, val)) in self.command_options['global'].items():
+ alias = self.negative_opt.get(opt)
+ if alias:
+ val = not strtobool(val)
+ elif opt in ('verbose', 'dry_run'): # ugh!
+ val = strtobool(val)
+
+ try:
+ setattr(self, alias or opt, val)
+ except ValueError as e:
+ raise DistutilsOptionError(e) from e
+
+ def warn_dash_deprecation(self, opt, section):
+ if section in (
+ 'options.extras_require',
+ 'options.data_files',
+ ):
+ return opt
+
+ underscore_opt = opt.replace('-', '_')
+ commands = list(itertools.chain(
+ distutils.command.__all__,
+ self._setuptools_commands(),
+ ))
+ if (
+ not section.startswith('options')
+ and section != 'metadata'
+ and section not in commands
+ ):
+ return underscore_opt
+
+ if '-' in opt:
+ SetuptoolsDeprecationWarning.emit(
+ "Invalid dash-separated options",
+ f"""
+ Usage of dash-separated {opt!r} will not be supported in future
+ versions. Please use the underscore name {underscore_opt!r} instead.
+ """,
+ see_docs="userguide/declarative_config.html",
+ due_date=(2023, 9, 26),
+ # Warning initially introduced in 3 Mar 2021
+ )
+ return underscore_opt
- def _set_command_options(self, command_obj, option_dict=None):
+ def _setuptools_commands(self):
+ try:
+ return metadata.distribution('setuptools').entry_points.names
+ except metadata.PackageNotFoundError:
+ # during bootstrapping, distribution doesn't exist
+ return []
+
+ def make_option_lowercase(self, opt, section):
+ if section != 'metadata' or opt.islower():
+ return opt
+
+ lowercase_opt = opt.lower()
+ SetuptoolsDeprecationWarning.emit(
+ "Invalid uppercase configuration",
+ f"""
+ Usage of uppercase key {opt!r} in {section!r} will not be supported in
+ future versions. Please use lowercase {lowercase_opt!r} instead.
+ """,
+ see_docs="userguide/declarative_config.html",
+ due_date=(2023, 9, 26),
+ # Warning initially introduced in 6 Mar 2021
+ )
+ return lowercase_opt
+
+ # FIXME: 'Distribution._set_command_options' is too complex (14)
+ def _set_command_options(self, command_obj, option_dict=None): # noqa: C901
"""
Set the options for 'command_obj' from 'option_dict'. Basically
this means copying elements of a dictionary ('option_dict') to
@@ -630,11 +845,9 @@ class Distribution(_Distribution):
self.announce(" setting options for '%s' command:" % command_name)
for (option, (source, value)) in option_dict.items():
if DEBUG:
- self.announce(" %s = %s (from %s)" % (option, value,
- source))
+ self.announce(" %s = %s (from %s)" % (option, value, source))
try:
- bool_opts = [translate_longopt(o)
- for o in command_obj.boolean_options]
+ bool_opts = [translate_longopt(o) for o in command_obj.boolean_options]
except AttributeError:
bool_opts = []
try:
@@ -653,31 +866,45 @@ class Distribution(_Distribution):
else:
raise DistutilsOptionError(
"error in %s: command '%s' has no such option '%s'"
- % (source, command_name, option))
+ % (source, command_name, option)
+ )
except ValueError as e:
raise DistutilsOptionError(e) from e
+ def _get_project_config_files(self, filenames):
+ """Add default file and split between INI and TOML"""
+ tomlfiles = []
+ standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")
+ if filenames is not None:
+ parts = partition(lambda f: Path(f).suffix == ".toml", filenames)
+ filenames = list(parts[0]) # 1st element => predicate is False
+ tomlfiles = list(parts[1]) # 2nd element => predicate is True
+ elif standard_project_metadata.exists():
+ tomlfiles = [standard_project_metadata]
+ return filenames, tomlfiles
+
def parse_config_files(self, filenames=None, ignore_option_errors=False):
"""Parses configuration files from various levels
and loads configuration.
-
"""
- self._parse_config_files(filenames=filenames)
+ inifiles, tomlfiles = self._get_project_config_files(filenames)
+
+ self._parse_config_files(filenames=inifiles)
+
+ setupcfg.parse_configuration(
+ self, self.command_options, ignore_option_errors=ignore_option_errors
+ )
+ for filename in tomlfiles:
+ pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
- parse_configuration(self, self.command_options,
- ignore_option_errors=ignore_option_errors)
self._finalize_requires()
+ self._finalize_license_files()
def fetch_build_eggs(self, requires):
"""Resolve pre-setup requirements"""
- resolved_dists = pkg_resources.working_set.resolve(
- pkg_resources.parse_requirements(requires),
- installer=self.fetch_build_egg,
- replace_conflicting=True,
- )
- for dist in resolved_dists:
- pkg_resources.working_set.add(dist, replace=True)
- return resolved_dists
+ from setuptools.installer import _fetch_build_eggs
+
+ return _fetch_build_eggs(self, requires)
def finalize_options(self):
"""
@@ -690,27 +917,33 @@ class Distribution(_Distribution):
def by_order(hook):
return getattr(hook, 'order', 0)
- eps = map(lambda e: e.load(), pkg_resources.iter_entry_points(group))
- for ep in sorted(eps, key=by_order):
+
+ defined = metadata.entry_points(group=group)
+ filtered = itertools.filterfalse(self._removed, defined)
+ loaded = map(lambda e: e.load(), filtered)
+ for ep in sorted(loaded, key=by_order):
ep(self)
+ @staticmethod
+ def _removed(ep):
+ """
+ When removing an entry point, if metadata is loaded
+ from an older version of Setuptools, that removed
+ entry point will attempt to be loaded and will fail.
+ See #2765 for more details.
+ """
+ removed = {
+ # removed 2021-09-05
+ '2to3_doctests',
+ }
+ return ep.name in removed
+
def _finalize_setup_keywords(self):
- for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
+ for ep in metadata.entry_points(group='distutils.setup_keywords'):
value = getattr(self, ep.name, None)
if value is not None:
- ep.require(installer=self.fetch_build_egg)
ep.load()(self, ep.name, value)
- def _finalize_2to3_doctests(self):
- if getattr(self, 'convert_2to3_doctests', None):
- # XXX may convert to set here when we can rely on set being builtin
- self.convert_2to3_doctests = [
- os.path.abspath(p)
- for p in self.convert_2to3_doctests
- ]
- else:
- self.convert_2to3_doctests = []
-
def get_egg_cache_dir(self):
egg_cache_dir = os.path.join(os.curdir, '.eggs')
if not os.path.exists(egg_cache_dir):
@@ -718,10 +951,14 @@ class Distribution(_Distribution):
windows_support.hide_file(egg_cache_dir)
readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')
with open(readme_txt_filename, 'w') as f:
- f.write('This directory contains eggs that were downloaded '
- 'by setuptools to build, test, and run plug-ins.\n\n')
- f.write('This directory caches those eggs to prevent '
- 'repeated downloads.\n\n')
+ f.write(
+ 'This directory contains eggs that were downloaded '
+ 'by setuptools to build, test, and run plug-ins.\n\n'
+ )
+ f.write(
+ 'This directory caches those eggs to prevent '
+ 'repeated downloads.\n\n'
+ )
f.write('However, it is safe to delete this directory.\n\n')
return egg_cache_dir
@@ -729,6 +966,7 @@ class Distribution(_Distribution):
def fetch_build_egg(self, req):
"""Fetch an egg needed for building"""
from setuptools.installer import fetch_build_egg
+
return fetch_build_egg(self, req)
def get_command_class(self, command):
@@ -736,27 +974,24 @@ class Distribution(_Distribution):
if command in self.cmdclass:
return self.cmdclass[command]
- eps = pkg_resources.iter_entry_points('distutils.commands', command)
+ eps = metadata.entry_points(group='distutils.commands', name=command)
for ep in eps:
- ep.require(installer=self.fetch_build_egg)
self.cmdclass[command] = cmdclass = ep.load()
return cmdclass
else:
return _Distribution.get_command_class(self, command)
def print_commands(self):
- for ep in pkg_resources.iter_entry_points('distutils.commands'):
+ for ep in metadata.entry_points(group='distutils.commands'):
if ep.name not in self.cmdclass:
- # don't require extras as the commands won't be invoked
- cmdclass = ep.resolve()
+ cmdclass = ep.load()
self.cmdclass[ep.name] = cmdclass
return _Distribution.print_commands(self)
def get_command_list(self):
- for ep in pkg_resources.iter_entry_points('distutils.commands'):
+ for ep in metadata.entry_points(group='distutils.commands'):
if ep.name not in self.cmdclass:
- # don't require extras as the commands won't be invoked
- cmdclass = ep.resolve()
+ cmdclass = ep.load()
self.cmdclass[ep.name] = cmdclass
return _Distribution.get_command_list(self)
@@ -788,19 +1023,18 @@ class Distribution(_Distribution):
pfx = package + '.'
if self.packages:
self.packages = [
- p for p in self.packages
- if p != package and not p.startswith(pfx)
+ p for p in self.packages if p != package and not p.startswith(pfx)
]
if self.py_modules:
self.py_modules = [
- p for p in self.py_modules
- if p != package and not p.startswith(pfx)
+ p for p in self.py_modules if p != package and not p.startswith(pfx)
]
if self.ext_modules:
self.ext_modules = [
- p for p in self.ext_modules
+ p
+ for p in self.ext_modules
if p.name != package and not p.name.startswith(pfx)
]
@@ -822,9 +1056,7 @@ class Distribution(_Distribution):
try:
old = getattr(self, name)
except AttributeError as e:
- raise DistutilsSetupError(
- "%s: No such distribution setting" % name
- ) from e
+ raise DistutilsSetupError("%s: No such distribution setting" % name) from e
if old is not None and not isinstance(old, sequence):
raise DistutilsSetupError(
name + ": this setting cannot be changed via include/exclude"
@@ -836,15 +1068,11 @@ class Distribution(_Distribution):
"""Handle 'include()' for list/tuple attrs without a special handler"""
if not isinstance(value, sequence):
- raise DistutilsSetupError(
- "%s: setting must be a list (%r)" % (name, value)
- )
+ raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value))
try:
old = getattr(self, name)
except AttributeError as e:
- raise DistutilsSetupError(
- "%s: No such distribution setting" % name
- ) from e
+ raise DistutilsSetupError("%s: No such distribution setting" % name) from e
if old is None:
setattr(self, name, value)
elif not isinstance(old, sequence):
@@ -897,6 +1125,7 @@ class Distribution(_Distribution):
src, alias = aliases[command]
del aliases[command] # ensure each alias can expand only once!
import shlex
+
args[:1] = shlex.split(alias, True)
command = args[0]
@@ -991,17 +1220,18 @@ class Distribution(_Distribution):
# Print metadata in UTF-8 no matter the platform
encoding = sys.stdout.encoding
- errors = sys.stdout.errors
- newline = sys.platform != 'win32' and '\n' or None
- line_buffering = sys.stdout.line_buffering
-
- sys.stdout = io.TextIOWrapper(
- sys.stdout.detach(), 'utf-8', errors, newline, line_buffering)
+ sys.stdout.reconfigure(encoding='utf-8')
try:
return _Distribution.handle_display_options(self, option_order)
finally:
- sys.stdout = io.TextIOWrapper(
- sys.stdout.detach(), encoding, errors, newline, line_buffering)
+ sys.stdout.reconfigure(encoding=encoding)
+
+ def run_command(self, command):
+ self.set_defaults()
+ # Postpone defaults until all explicit configuration is considered
+ # (setup() args, config files, command line and plugins)
+
+ super().run_command(command)
class DistDeprecationWarning(SetuptoolsDeprecationWarning):