summaryrefslogtreecommitdiff
path: root/setuptools/dist.py
diff options
context:
space:
mode:
Diffstat (limited to 'setuptools/dist.py')
-rw-r--r--setuptools/dist.py187
1 files changed, 133 insertions, 54 deletions
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 848d6b0f..5507167d 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -19,17 +19,20 @@ from glob import iglob
import itertools
import textwrap
from typing import List, Optional, 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 setuptools.extern.more_itertools import unique_everseen
+from setuptools.extern.more_itertools import unique_everseen, partition
+from setuptools.extern import nspektr
+
+from ._importlib import metadata
from . import SetuptoolsDeprecationWarning
@@ -37,8 +40,13 @@ import setuptools
import setuptools.command
from setuptools import windows_support
from setuptools.monkey import get_unpatched
-from setuptools.config import parse_configuration
+from setuptools.config import setupcfg, pyprojecttoml
+from setuptools.discovery import ConfigDiscovery
+
import pkg_resources
+from setuptools.extern.packaging import version
+from . import _reqs
+from . import _entry_points
if TYPE_CHECKING:
from email.message import Message
@@ -55,7 +63,7 @@ def _get_unpatched(cls):
def get_metadata_version(self):
mv = getattr(self, 'metadata_version', None)
if mv is None:
- mv = StrictVersion('2.1')
+ mv = version.Version('2.1')
self.metadata_version = mv
return mv
@@ -94,7 +102,7 @@ def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]:
def _read_payload_from_msg(msg: "Message") -> Optional[str]:
value = msg.get_payload().strip()
- if value == 'UNKNOWN':
+ if value == 'UNKNOWN' or not value:
return None
return value
@@ -103,7 +111,7 @@ def read_pkg_file(self, file):
"""Reads the metadata values from a file object."""
msg = message_from_file(file)
- self.metadata_version = StrictVersion(msg['metadata-version'])
+ 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')
@@ -113,15 +121,14 @@ def read_pkg_file(self, file):
self.author_email = _read_field_from_msg(msg, 'author-email')
self.maintainer_email = None
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')
- if 'download-url' in msg:
- self.download_url = _read_field_from_msg(msg, 'download-url')
- else:
- self.download_url = None
-
self.long_description = _read_field_unescaped_from_msg(msg, 'description')
- if self.long_description is None and self.metadata_version >= StrictVersion('2.1'):
+ 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')
@@ -132,7 +139,7 @@ def read_pkg_file(self, file):
self.classifiers = _read_list_from_msg(msg, 'classifier')
# PEP 314 - these fields only exist in 1.1
- if self.metadata_version == StrictVersion('1.1'):
+ 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')
@@ -145,11 +152,14 @@ def read_pkg_file(self, file):
def single_line(val):
- """Validate that the value does not have line breaks."""
- # Ref: https://github.com/pypa/setuptools/issues/1390
+ """
+ Quick and dirty validation for Summary pypa/setuptools#1390.
+ """
if '\n' in val:
- raise ValueError('Newlines are not allowed')
-
+ # TODO: Replace with `raise ValueError("newlines not allowed")`
+ # after reviewing #2893.
+ warnings.warn("newlines not allowed and will break in the future")
+ val = val.strip().split('\n')[0]
return val
@@ -164,10 +174,14 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
write_field('Metadata-Version', str(version))
write_field('Name', self.get_name())
write_field('Version', self.get_version())
- write_field('Summary', single_line(self.get_description()))
- write_field('Home-page', self.get_url())
+
+ 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'),
@@ -179,10 +193,10 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
if attr_val is not None:
write_field(field, attr_val)
- license = rfc822_escape(self.get_license())
- write_field('License', license)
- if self.download_url:
- write_field('Download-URL', self.download_url)
+ license = self.get_license()
+ if license:
+ write_field('License', rfc822_escape(license))
+
for project_url in self.project_urls.items():
write_field('Project-URL', '%s, %s' % project_url)
@@ -190,7 +204,8 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
if keywords:
write_field('Keywords', keywords)
- for platform in self.get_platforms():
+ platforms = self.get_platforms() or []
+ for platform in platforms:
write_field('Platform', platform)
self._write_list(file, 'Classifier', self.get_classifiers())
@@ -213,7 +228,11 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME
self._write_list(file, 'License-File', self.license_files or [])
- file.write("\n%s\n\n" % self.get_long_description())
+ 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
@@ -221,7 +240,7 @@ 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(
@@ -279,7 +298,7 @@ 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))
+ list(_reqs.parse(reqs))
def assert_bool(dist, attr, value):
@@ -299,7 +318,7 @@ def invalid_unless_false(dist, attr, value):
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:
@@ -324,8 +343,8 @@ def check_specifier(dist, attr, value):
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
@@ -448,7 +467,7 @@ 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,
@@ -459,6 +478,13 @@ class Distribution(_Distribution):
},
)
+ # 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(
@@ -466,6 +492,19 @@ class Distribution(_Distribution):
)
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.
@@ -516,6 +555,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
@@ -524,6 +565,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()
@@ -534,11 +579,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)
@@ -564,7 +610,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))
@@ -572,7 +618,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()
)
@@ -705,7 +752,10 @@ class Distribution(_Distribution):
return opt
underscore_opt = opt.replace('-', '_')
- commands = distutils.command.__all__ + self._setuptools_commands()
+ commands = list(itertools.chain(
+ distutils.command.__all__,
+ self._setuptools_commands(),
+ ))
if (
not section.startswith('options')
and section != 'metadata'
@@ -723,9 +773,8 @@ class Distribution(_Distribution):
def _setuptools_commands(self):
try:
- dist = pkg_resources.get_distribution('setuptools')
- return list(dist.get_entry_map('distutils.commands'))
- except pkg_resources.DistributionNotFound:
+ return metadata.distribution('setuptools').entry_points.names
+ except metadata.PackageNotFoundError:
# during bootstrapping, distribution doesn't exist
return []
@@ -788,23 +837,39 @@ class Distribution(_Distribution):
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)
- parse_configuration(
+ 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)
+
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),
+ _reqs.parse(requires),
installer=self.fetch_build_egg,
replace_conflicting=True,
)
@@ -824,7 +889,7 @@ class Distribution(_Distribution):
def by_order(hook):
return getattr(hook, 'order', 0)
- defined = pkg_resources.iter_entry_points(group)
+ 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):
@@ -845,12 +910,21 @@ class Distribution(_Distribution):
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)
+ self._install_dependencies(ep)
ep.load()(self, ep.name, value)
+ def _install_dependencies(self, ep):
+ """
+ Given an entry point, ensure that any declared extras for
+ its distribution are installed.
+ """
+ for req in nspektr.missing(ep):
+ # fetch_build_egg expects pkg_resources.Requirement
+ self.fetch_build_egg(pkg_resources.Requirement(str(req)))
+
def get_egg_cache_dir(self):
egg_cache_dir = os.path.join(os.curdir, '.eggs')
if not os.path.exists(egg_cache_dir):
@@ -881,27 +955,25 @@ 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._install_dependencies(ep)
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)
@@ -1144,6 +1216,13 @@ class Distribution(_Distribution):
sys.stdout.detach(), encoding, errors, newline, line_buffering
)
+ 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):
"""Class for warning about deprecations in dist in