From 3b74d678d558ec43d53fd4c609871853a2d0865e Mon Sep 17 00:00:00 2001 From: DEKHTIARJonathan Date: Thu, 23 Jan 2025 17:21:58 -0500 Subject: [PATCH] [PEP 771] --- src/pip/__init__.py | 2 +- src/pip/_internal/metadata/_json.py | 1 + src/pip/_internal/metadata/base.py | 15 +++++++++ .../_internal/metadata/importlib/_dists.py | 11 ++++--- src/pip/_internal/metadata/pkg_resources.py | 6 ++++ src/pip/_internal/operations/prepare.py | 6 ++++ .../resolution/resolvelib/candidates.py | 2 ++ .../resolution/resolvelib/factory.py | 4 +++ src/pip/_vendor/distlib/metadata.py | 33 ++++++++++++++----- src/pip/_vendor/packaging/metadata.py | 13 ++++++-- src/pip/_vendor/pkg_resources/__init__.py | 6 ++++ src/pip/_vendor/resolvelib/resolvers.py | 7 +++- 12 files changed, 89 insertions(+), 17 deletions(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 4eff4299c01..58a3b4dc80a 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1,6 +1,6 @@ from typing import List, Optional -__version__ = "25.0.dev0" +__version__ = "25.0.dev0+pep-771" def main(args: Optional[List[str]] = None) -> int: diff --git a/src/pip/_internal/metadata/_json.py b/src/pip/_internal/metadata/_json.py index f3aeab3225f..227818358a7 100644 --- a/src/pip/_internal/metadata/_json.py +++ b/src/pip/_internal/metadata/_json.py @@ -30,6 +30,7 @@ ("Requires-Python", False), ("Requires-External", True), ("Project-URL", True), + ("Default-Extra", True), ("Provides-Extra", True), ("Provides-Dist", True), ("Obsoletes-Dist", True), diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 9eabcdb278b..f9d819be709 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -442,6 +442,9 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen For modern .dist-info distributions, this is the collection of "Requires-Dist:" entries in distribution metadata. + + In case, no "Extra" is specified, will use "Default-Extra" as specified + per PEP 771. """ raise NotImplementedError() @@ -449,6 +452,18 @@ def iter_raw_dependencies(self) -> Iterable[str]: """Raw Requires-Dist metadata.""" return self.metadata.get_all("Requires-Dist", []) + def iter_default_extras(self) -> Iterable[NormalizedName]: + """Extras provided by this distribution. + + For modern .dist-info distributions, this is the collection of + "Default-Extra:" entries in distribution metadata. + + The return value of this function is expected to be normalised names, + per PEP 771, with the returned value being handled appropriately by + `iter_dependencies`. + """ + raise NotImplementedError() + def iter_provided_extras(self) -> Iterable[NormalizedName]: """Extras provided by this distribution. diff --git a/src/pip/_internal/metadata/importlib/_dists.py b/src/pip/_internal/metadata/importlib/_dists.py index 6f2d674efcc..b81bf4d67af 100644 --- a/src/pip/_internal/metadata/importlib/_dists.py +++ b/src/pip/_internal/metadata/importlib/_dists.py @@ -207,19 +207,22 @@ def _metadata_impl(self) -> email.message.Message: # until upstream can improve the protocol. (python/cpython#94952) return cast(email.message.Message, self._dist.metadata) - def iter_provided_extras(self) -> Iterable[NormalizedName]: + def iter_default_extras(self) -> Iterable[NormalizedName]: return [ canonicalize_name(extra) - for extra in self.metadata.get_all("Provides-Extra", []) + for extra in self.metadata.get_all("Default-Extra", []) ] - def iter_default_extras(self) -> Iterable[NormalizedName]: + def iter_provided_extras(self) -> Iterable[NormalizedName]: return [ canonicalize_name(extra) - for extra in self.metadata.get_all("Default-Extra", []) + for extra in self.metadata.get_all("Provides-Extra", []) ] def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]: + if not extras: + extras = list(self.iter_default_extras()) + contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras] for req_string in self.metadata.get_all("Requires-Dist", []): # strip() because email.message.Message.get_all() may return a leading \n diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 4ea84f93a6f..8d97a386bdf 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -239,13 +239,19 @@ def _metadata_impl(self) -> email.message.Message: return feed_parser.close() def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]: + extras = extras or self._dist.default_extras_require + if extras: relevant_extras = set(self._extra_mapping) & set( map(canonicalize_name, extras) ) extras = [self._extra_mapping[extra] for extra in relevant_extras] + return self._dist.requires(extras) + def iter_default_extras(self) -> Iterable[NormalizedName]: + return self._dist.default_extras_require or [] + def iter_provided_extras(self) -> Iterable[NormalizedName]: return self._extra_mapping.keys() diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e6aa3447200..eadebba38de 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -646,6 +646,12 @@ def _prepare_linked_requirement( self.build_isolation, self.check_build_deps, ) + + # Setting up the default-extra if necessary + default_extras = frozenset(dist.metadata.get_all("Default-Extra", [])) + req.extras = req.extras or default_extras + req.req.extras = req.extras or default_extras + return dist def save_linked_requirement(self, req: InstallRequirement) -> None: diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 6617644fe53..7f5a62b08e6 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -310,6 +310,8 @@ def __init__( version=version, ) + template.extras = ireq.extras + def _prepare_distribution(self) -> BaseDistribution: preparer = self._factory.preparer return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0227dab910b..a71f5f912b9 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -187,6 +187,7 @@ def _make_candidate_from_link( base: Optional[BaseCandidate] = self._make_base_candidate_from_link( link, template, name, version ) + extras = extras if extras else template.extras if not extras or base is None: return base return self._make_extras_candidate(base, extras, comes_from=template) @@ -482,6 +483,9 @@ def _make_requirements_from_install_req( (or link) and one with the extra. This allows centralized constraint handling for the base, resulting in fewer candidate rejections. """ + if ireq.comes_from is not None: + requested_extras = requested_extras or ireq.comes_from.extras + if not ireq.match_markers(requested_extras): logger.info( "Ignoring %s: markers '%s' don't match your environment", diff --git a/src/pip/_vendor/distlib/metadata.py b/src/pip/_vendor/distlib/metadata.py index ce9a34b3e24..be3d4e6f60f 100644 --- a/src/pip/_vendor/distlib/metadata.py +++ b/src/pip/_vendor/distlib/metadata.py @@ -5,7 +5,7 @@ # """Implementation of the Metadata for Python packages PEPs. -Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2). +Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1, 2.2, 2.3, 2.4 and 2.5). """ from __future__ import unicode_literals @@ -89,6 +89,12 @@ class MetadataInvalidError(DistlibException): _643_FIELDS = _566_FIELDS + _643_MARKERS +# PEP 771 +_771_MARKERS = ('Default-Extra') + +_771_FIELDS = _643_FIELDS + _771_MARKERS + + _ALL_FIELDS = set() _ALL_FIELDS.update(_241_FIELDS) _ALL_FIELDS.update(_314_FIELDS) @@ -96,6 +102,7 @@ class MetadataInvalidError(DistlibException): _ALL_FIELDS.update(_426_FIELDS) _ALL_FIELDS.update(_566_FIELDS) _ALL_FIELDS.update(_643_FIELDS) +_ALL_FIELDS.update(_771_FIELDS) EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''') @@ -115,6 +122,8 @@ def _version2fieldlist(version): # return _426_FIELDS elif version == '2.2': return _643_FIELDS + elif version == '2.5': + return _771_FIELDS raise MetadataUnrecognizedVersionError(version) @@ -125,7 +134,7 @@ def _has_marker(keys, markers): return any(marker in keys for marker in markers) keys = [key for key, value in fields.items() if value not in ([], 'UNKNOWN', None)] - possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2'] # 2.0 removed + possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2', '2.5'] # 2.0 removed # first let's try to see if a field is not part of one of the version for key in keys: @@ -148,6 +157,9 @@ def _has_marker(keys, markers): if key not in _643_FIELDS and '2.2' in possible_versions: possible_versions.remove('2.2') logger.debug('Removed 2.2 due to %s', key) + if key not in _771_FIELDS and '2.5' in possible_versions: + possible_versions.remove('2.5') + logger.debug('Removed 2.5 due to %s', key) # if key not in _426_FIELDS and '2.0' in possible_versions: # possible_versions.remove('2.0') # logger.debug('Removed 2.0 due to %s', key) @@ -165,16 +177,19 @@ def _has_marker(keys, markers): is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS) # is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS) is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS) - if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1: - raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields') + is_2_5 = '2.5' in possible_versions and _has_marker(keys, _771_MARKERS) + + if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) + int(is_2_5) > 1: + raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2/2.5 fields') # we have the choice, 1.0, or 1.2, 2.1 or 2.2 # - 1.0 has a broken Summary field but works with all tools # - 1.1 is to avoid # - 1.2 fixes Summary but has little adoption # - 2.1 adds more features - # - 2.2 is the latest - if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2: + # - 2.2 adds more features + # - 2.5 is the latest + if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2 and not is_2_5: # we couldn't find any specific marker if PKG_INFO_PREFERRED_VERSION in possible_versions: return PKG_INFO_PREFERRED_VERSION @@ -184,10 +199,10 @@ def _has_marker(keys, markers): return '1.2' if is_2_1: return '2.1' - # if is_2_2: - # return '2.2' + if is_2_2: + return '2.2' - return '2.2' + return '2.5' # This follows the rules about transforming keys as described in diff --git a/src/pip/_vendor/packaging/metadata.py b/src/pip/_vendor/packaging/metadata.py index 721f411cfc4..5f4f7589556 100644 --- a/src/pip/_vendor/packaging/metadata.py +++ b/src/pip/_vendor/packaging/metadata.py @@ -132,6 +132,9 @@ class RawMetadata(TypedDict, total=False): license_expression: str license_files: list[str] + # Metadata 2.5 - PEP 771 + default_extra: list[str] + _STRING_FIELDS = { "author", @@ -463,8 +466,8 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]: # Keep the two values in sync. -_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"] -_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"] +_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] +_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"] _REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) @@ -861,3 +864,9 @@ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata: """``Provides`` (deprecated)""" obsoletes: _Validator[list[str] | None] = _Validator(added="1.1") """``Obsoletes`` (deprecated)""" + # PEP 771 lets us define a default `extras_require` if none is passed by the + # user. + default_extra: _Validator[list[utils.NormalizedName] | None] = _Validator( + added="2.5", + ) + """:external:ref:`core-metadata-default-extra`""" diff --git a/src/pip/_vendor/pkg_resources/__init__.py b/src/pip/_vendor/pkg_resources/__init__.py index 57ce7f10064..0d72111d822 100644 --- a/src/pip/_vendor/pkg_resources/__init__.py +++ b/src/pip/_vendor/pkg_resources/__init__.py @@ -3070,6 +3070,8 @@ def requires(self, extras: Iterable[str] = ()): dm = self._dep_map deps: list[Requirement] = [] deps.extend(dm.get(None, ())) + + extras = extras or self.default_extras_require for ext in extras: try: deps.extend(dm[safe_extra(ext)]) @@ -3322,6 +3324,10 @@ def clone(self, **kw: str | int | IResourceProvider | None): def extras(self): return [dep for dep in self._dep_map if dep] + @property + def default_extras_require(self): + return self._parsed_pkg_info.get_all('Default-Extra') or [] + class EggInfoDistribution(Distribution): def _reload_version(self): diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index 2c3d0e306f9..116c19c3983 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -1,4 +1,5 @@ import collections +import contextlib import itertools import operator @@ -172,7 +173,11 @@ def _add_to_criteria(self, criteria, requirement, parent): ) if not criterion.candidates: raise RequirementsConflicted(criterion) - criteria[identifier] = criterion + + with contextlib.suppress(AttributeError): + requirement._extras = requirement._ireq.extras + + criteria[requirement.name] = criterion def _remove_information_from_criteria(self, criteria, parents): """Remove information from parents of criteria.