Skip to content

Commit 07d9104

Browse files
[PEP 771]
1 parent 0b59b51 commit 07d9104

File tree

13 files changed

+92
-19
lines changed

13 files changed

+92
-19
lines changed

src/pip/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List, Optional
22

3-
__version__ = "25.0.dev0"
3+
__version__ = "25.0.dev0+pep-771"
44

55

66
def main(args: Optional[List[str]] = None) -> int:

src/pip/_internal/metadata/_json.py

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
("Requires-Python", False),
3131
("Requires-External", True),
3232
("Project-URL", True),
33+
("Default-Extra", True),
3334
("Provides-Extra", True),
3435
("Provides-Dist", True),
3536
("Obsoletes-Dist", True),

src/pip/_internal/metadata/base.py

+15
Original file line numberDiff line numberDiff line change
@@ -442,13 +442,28 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen
442442
443443
For modern .dist-info distributions, this is the collection of
444444
"Requires-Dist:" entries in distribution metadata.
445+
446+
In case, no "Extra" is specified, will use "Default-Extra" as specified
447+
per PEP 771.
445448
"""
446449
raise NotImplementedError()
447450

448451
def iter_raw_dependencies(self) -> Iterable[str]:
449452
"""Raw Requires-Dist metadata."""
450453
return self.metadata.get_all("Requires-Dist", [])
451454

455+
def iter_default_extras(self) -> Iterable[NormalizedName]:
456+
"""Extras provided by this distribution.
457+
458+
For modern .dist-info distributions, this is the collection of
459+
"Default-Extra:" entries in distribution metadata.
460+
461+
The return value of this function is expected to be normalised names,
462+
per PEP 771, with the returned value being handled appropriately by
463+
`iter_dependencies`.
464+
"""
465+
raise NotImplementedError()
466+
452467
def iter_provided_extras(self) -> Iterable[NormalizedName]:
453468
"""Extras provided by this distribution.
454469

src/pip/_internal/metadata/importlib/_dists.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -207,19 +207,22 @@ def _metadata_impl(self) -> email.message.Message:
207207
# until upstream can improve the protocol. (python/cpython#94952)
208208
return cast(email.message.Message, self._dist.metadata)
209209

210-
def iter_provided_extras(self) -> Iterable[NormalizedName]:
210+
def iter_default_extras(self) -> Iterable[NormalizedName]:
211211
return [
212212
canonicalize_name(extra)
213-
for extra in self.metadata.get_all("Provides-Extra", [])
213+
for extra in self.metadata.get_all("Default-Extra", [])
214214
]
215215

216-
def iter_default_extras(self) -> Iterable[NormalizedName]:
216+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
217217
return [
218218
canonicalize_name(extra)
219-
for extra in self.metadata.get_all("Default-Extra", [])
219+
for extra in self.metadata.get_all("Provides-Extra", [])
220220
]
221221

222222
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
223+
if not extras:
224+
extras = list(self.iter_default_extras())
225+
223226
contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
224227
for req_string in self.metadata.get_all("Requires-Dist", []):
225228
# strip() because email.message.Message.get_all() may return a leading \n

src/pip/_internal/metadata/pkg_resources.py

+6
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,19 @@ def _metadata_impl(self) -> email.message.Message:
239239
return feed_parser.close()
240240

241241
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
242+
extras = extras or self._dist.default_extras_require
243+
242244
if extras:
243245
relevant_extras = set(self._extra_mapping) & set(
244246
map(canonicalize_name, extras)
245247
)
246248
extras = [self._extra_mapping[extra] for extra in relevant_extras]
249+
247250
return self._dist.requires(extras)
248251

252+
def iter_default_extras(self) -> Iterable[NormalizedName]:
253+
return self._dist.default_extras_require or []
254+
249255
def iter_provided_extras(self) -> Iterable[NormalizedName]:
250256
return self._extra_mapping.keys()
251257

src/pip/_internal/operations/prepare.py

+6
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,12 @@ def _prepare_linked_requirement(
646646
self.build_isolation,
647647
self.check_build_deps,
648648
)
649+
650+
# Setting up the default-extra if necessary
651+
default_extras = frozenset(dist.metadata.get_all("Default-Extra", []))
652+
req.extras = req.extras or default_extras
653+
req.req.extras = req.extras or default_extras
654+
649655
return dist
650656

651657
def save_linked_requirement(self, req: InstallRequirement) -> None:

src/pip/_internal/resolution/resolvelib/candidates.py

+2
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ def __init__(
310310
version=version,
311311
)
312312

313+
template.extras = ireq.extras
314+
313315
def _prepare_distribution(self) -> BaseDistribution:
314316
preparer = self._factory.preparer
315317
return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)

src/pip/_internal/resolution/resolvelib/factory.py

+5
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,12 @@ def _make_candidate_from_dist(
172172
except KeyError:
173173
base = AlreadyInstalledCandidate(dist, template, factory=self)
174174
self._installed_candidate_cache[dist.canonical_name] = base
175+
extras = (
176+
extras if extras else frozenset(dist.metadata.get_all("Default-Extra", []))
177+
)
175178
if not extras:
176179
return base
180+
177181
return self._make_extras_candidate(base, extras, comes_from=template)
178182

179183
def _make_candidate_from_link(
@@ -187,6 +191,7 @@ def _make_candidate_from_link(
187191
base: Optional[BaseCandidate] = self._make_base_candidate_from_link(
188192
link, template, name, version
189193
)
194+
extras = extras if extras else template.extras
190195
if not extras or base is None:
191196
return base
192197
return self._make_extras_candidate(base, extras, comes_from=template)

src/pip/_internal/resolution/resolvelib/requirements.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ def get_candidate_lookup(self) -> CandidateLookup:
107107
return None, self._ireq
108108

109109
def is_satisfied_by(self, candidate: Candidate) -> bool:
110-
assert candidate.name == self.name, (
110+
assert candidate.project_name == self.project_name, (
111111
f"Internal issue: Candidate is not for this requirement "
112-
f"{candidate.name} vs {self.name}"
112+
f"{candidate.project_name} vs {self.project_name}"
113113
)
114114
# We can safely always allow prereleases here since PackageFinder
115115
# already implements the prerelease logic, and would have filtered out

src/pip/_vendor/distlib/metadata.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#
66
"""Implementation of the Metadata for Python packages PEPs.
77
8-
Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and 2.2).
8+
Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1, 2.2, 2.3, 2.4 and 2.5).
99
"""
1010
from __future__ import unicode_literals
1111

@@ -89,13 +89,20 @@ class MetadataInvalidError(DistlibException):
8989

9090
_643_FIELDS = _566_FIELDS + _643_MARKERS
9191

92+
# PEP 771
93+
_771_MARKERS = ('Default-Extra')
94+
95+
_771_FIELDS = _643_FIELDS + _771_MARKERS
96+
97+
9298
_ALL_FIELDS = set()
9399
_ALL_FIELDS.update(_241_FIELDS)
94100
_ALL_FIELDS.update(_314_FIELDS)
95101
_ALL_FIELDS.update(_345_FIELDS)
96102
_ALL_FIELDS.update(_426_FIELDS)
97103
_ALL_FIELDS.update(_566_FIELDS)
98104
_ALL_FIELDS.update(_643_FIELDS)
105+
_ALL_FIELDS.update(_771_FIELDS)
99106

100107
EXTRA_RE = re.compile(r'''extra\s*==\s*("([^"]+)"|'([^']+)')''')
101108

@@ -115,6 +122,8 @@ def _version2fieldlist(version):
115122
# return _426_FIELDS
116123
elif version == '2.2':
117124
return _643_FIELDS
125+
elif version == '2.5':
126+
return _771_FIELDS
118127
raise MetadataUnrecognizedVersionError(version)
119128

120129

@@ -125,7 +134,7 @@ def _has_marker(keys, markers):
125134
return any(marker in keys for marker in markers)
126135

127136
keys = [key for key, value in fields.items() if value not in ([], 'UNKNOWN', None)]
128-
possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2'] # 2.0 removed
137+
possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2', '2.5'] # 2.0 removed
129138

130139
# first let's try to see if a field is not part of one of the version
131140
for key in keys:
@@ -148,6 +157,9 @@ def _has_marker(keys, markers):
148157
if key not in _643_FIELDS and '2.2' in possible_versions:
149158
possible_versions.remove('2.2')
150159
logger.debug('Removed 2.2 due to %s', key)
160+
if key not in _771_FIELDS and '2.5' in possible_versions:
161+
possible_versions.remove('2.5')
162+
logger.debug('Removed 2.5 due to %s', key)
151163
# if key not in _426_FIELDS and '2.0' in possible_versions:
152164
# possible_versions.remove('2.0')
153165
# logger.debug('Removed 2.0 due to %s', key)
@@ -165,16 +177,19 @@ def _has_marker(keys, markers):
165177
is_2_1 = '2.1' in possible_versions and _has_marker(keys, _566_MARKERS)
166178
# is_2_0 = '2.0' in possible_versions and _has_marker(keys, _426_MARKERS)
167179
is_2_2 = '2.2' in possible_versions and _has_marker(keys, _643_MARKERS)
168-
if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) > 1:
169-
raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2 fields')
180+
is_2_5 = '2.5' in possible_versions and _has_marker(keys, _771_MARKERS)
181+
182+
if int(is_1_1) + int(is_1_2) + int(is_2_1) + int(is_2_2) + int(is_2_5) > 1:
183+
raise MetadataConflictError('You used incompatible 1.1/1.2/2.1/2.2/2.5 fields')
170184

171185
# we have the choice, 1.0, or 1.2, 2.1 or 2.2
172186
# - 1.0 has a broken Summary field but works with all tools
173187
# - 1.1 is to avoid
174188
# - 1.2 fixes Summary but has little adoption
175189
# - 2.1 adds more features
176-
# - 2.2 is the latest
177-
if not is_1_1 and not is_1_2 and not is_2_1 and not is_2_2:
190+
# - 2.2 adds more features
191+
# - 2.5 is the latest
192+
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:
178193
# we couldn't find any specific marker
179194
if PKG_INFO_PREFERRED_VERSION in possible_versions:
180195
return PKG_INFO_PREFERRED_VERSION
@@ -184,10 +199,10 @@ def _has_marker(keys, markers):
184199
return '1.2'
185200
if is_2_1:
186201
return '2.1'
187-
# if is_2_2:
188-
# return '2.2'
202+
if is_2_2:
203+
return '2.2'
189204

190-
return '2.2'
205+
return '2.5'
191206

192207

193208
# This follows the rules about transforming keys as described in

src/pip/_vendor/packaging/metadata.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ class RawMetadata(TypedDict, total=False):
132132
license_expression: str
133133
license_files: list[str]
134134

135+
# Metadata 2.5 - PEP 771
136+
default_extra: list[str]
137+
135138

136139
_STRING_FIELDS = {
137140
"author",
@@ -463,8 +466,8 @@ def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
463466

464467

465468
# Keep the two values in sync.
466-
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
467-
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4"]
469+
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
470+
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
468471

469472
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
470473

@@ -861,3 +864,9 @@ def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:
861864
"""``Provides`` (deprecated)"""
862865
obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
863866
"""``Obsoletes`` (deprecated)"""
867+
# PEP 771 lets us define a default `extras_require` if none is passed by the
868+
# user.
869+
default_extra: _Validator[list[utils.NormalizedName] | None] = _Validator(
870+
added="2.5",
871+
)
872+
""":external:ref:`core-metadata-default-extra`"""

src/pip/_vendor/pkg_resources/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -3070,6 +3070,8 @@ def requires(self, extras: Iterable[str] = ()):
30703070
dm = self._dep_map
30713071
deps: list[Requirement] = []
30723072
deps.extend(dm.get(None, ()))
3073+
3074+
extras = extras or self.default_extras_require
30733075
for ext in extras:
30743076
try:
30753077
deps.extend(dm[safe_extra(ext)])
@@ -3322,6 +3324,10 @@ def clone(self, **kw: str | int | IResourceProvider | None):
33223324
def extras(self):
33233325
return [dep for dep in self._dep_map if dep]
33243326

3327+
@property
3328+
def default_extras_require(self):
3329+
return self._parsed_pkg_info.get_all('Default-Extra') or []
3330+
33253331

33263332
class EggInfoDistribution(Distribution):
33273333
def _reload_version(self):

src/pip/_vendor/resolvelib/resolvers.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import collections
2+
import contextlib
23
import itertools
34
import operator
45

@@ -172,7 +173,11 @@ def _add_to_criteria(self, criteria, requirement, parent):
172173
)
173174
if not criterion.candidates:
174175
raise RequirementsConflicted(criterion)
175-
criteria[identifier] = criterion
176+
177+
with contextlib.suppress(AttributeError):
178+
requirement._extras = requirement._ireq.extras
179+
180+
criteria[requirement.name] = criterion
176181

177182
def _remove_information_from_criteria(self, criteria, parents):
178183
"""Remove information from parents of criteria.

0 commit comments

Comments
 (0)