Skip to content

Commit 0b6ebfa

Browse files
committed
Implement PEP 685 on distribution objects directly
This uses normalised names across the board for extras, with comparisions outside this context relying on `packaging`'s support for the corresponding comparisions.
1 parent ccbb835 commit 0b6ebfa

File tree

4 files changed

+36
-75
lines changed

4 files changed

+36
-75
lines changed

src/pip/_internal/metadata/base.py

+4-13
Original file line numberDiff line numberDiff line change
@@ -452,24 +452,15 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen
452452
"""
453453
raise NotImplementedError()
454454

455-
def iter_provided_extras(self) -> Iterable[str]:
455+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
456456
"""Extras provided by this distribution.
457457
458458
For modern .dist-info distributions, this is the collection of
459459
"Provides-Extra:" entries in distribution metadata.
460460
461-
The return value of this function is not particularly useful other than
462-
display purposes due to backward compatibility issues and the extra
463-
names being poorly normalized prior to PEP 685. If you want to perform
464-
logic operations on extras, use :func:`is_extra_provided` instead.
465-
"""
466-
raise NotImplementedError()
467-
468-
def is_extra_provided(self, extra: str) -> bool:
469-
"""Check whether an extra is provided by this distribution.
470-
471-
This is needed mostly for compatibility issues with pkg_resources not
472-
following the extra normalization rules defined in PEP 685.
461+
The return value of this function is expected to be normalised names,
462+
per PEP 685, with the returned value being handled appropriately by
463+
`iter_dependencies`.
473464
"""
474465
raise NotImplementedError()
475466

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

+5-8
Original file line numberDiff line numberDiff line change
@@ -206,14 +206,11 @@ def _metadata_impl(self) -> email.message.Message:
206206
# until upstream can improve the protocol. (python/cpython#94952)
207207
return cast(email.message.Message, self._dist.metadata)
208208

209-
def iter_provided_extras(self) -> Iterable[str]:
210-
return self.metadata.get_all("Provides-Extra", [])
211-
212-
def is_extra_provided(self, extra: str) -> bool:
213-
return any(
214-
canonicalize_name(provided_extra) == canonicalize_name(extra)
215-
for provided_extra in self.metadata.get_all("Provides-Extra", [])
216-
)
209+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
210+
return [
211+
canonicalize_name(extra)
212+
for extra in self.metadata.get_all("Provides-Extra", [])
213+
]
217214

218215
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
219216
contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]

src/pip/_internal/metadata/pkg_resources.py

+15-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
import logging
44
import os
55
import zipfile
6-
from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
6+
from typing import (
7+
Collection,
8+
Iterable,
9+
Iterator,
10+
List,
11+
Mapping,
12+
NamedTuple,
13+
Optional,
14+
cast,
15+
)
716

817
from pip._vendor import pkg_resources
918
from pip._vendor.packaging.requirements import Requirement
@@ -77,13 +86,13 @@ def __init__(self, dist: pkg_resources.Distribution) -> None:
7786
self._dist = dist
7887
# This is populated lazily, to avoid loading metadata for all possible
7988
# distributions eagerly.
80-
self.__extra_mapping = None
89+
self.__extra_mapping: Mapping[NormalizedName, str] | None = None
8190

8291
@property
83-
def _extra_mapping(self):
92+
def _extra_mapping(self) -> Mapping[NormalizedName, str]:
8493
if self.__extra_mapping is None:
8594
self.__extra_mapping = {
86-
canonicalize_name(extra): pkg_resources.safe_extra(extra)
95+
canonicalize_name(extra): pkg_resources.safe_extra(cast(str, extra))
8796
for extra in self.metadata.get_all("Provides-Extra", [])
8897
}
8998

@@ -235,11 +244,8 @@ def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requiremen
235244
extras = [self._extra_mapping[extra] for extra in relevant_extras]
236245
return self._dist.requires(extras)
237246

238-
def iter_provided_extras(self) -> Iterable[str]:
239-
return self._dist.extras
240-
241-
def is_extra_provided(self, extra: str) -> bool:
242-
return canonicalize_name(extra) in self._extra_mapping
247+
def iter_provided_extras(self) -> Iterable[NormalizedName]:
248+
return list(self._extra_mapping.keys())
243249

244250

245251
class Environment(BaseEnvironment):

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

+12-45
Original file line numberDiff line numberDiff line change
@@ -491,50 +491,6 @@ def is_editable(self) -> bool:
491491
def source_link(self) -> Optional[Link]:
492492
return self.base.source_link
493493

494-
def _warn_invalid_extras(
495-
self,
496-
requested: FrozenSet[str],
497-
valid: FrozenSet[str],
498-
) -> None:
499-
"""Emit warnings for invalid extras being requested.
500-
501-
This emits a warning for each requested extra that is not in the
502-
candidate's ``Provides-Extra`` list.
503-
"""
504-
invalid_extras_to_warn = frozenset(
505-
extra
506-
for extra in requested
507-
if extra not in valid
508-
# If an extra is requested in an unnormalized form, skip warning
509-
# about the normalized form being missing.
510-
and extra in self.extras
511-
)
512-
if not invalid_extras_to_warn:
513-
return
514-
for extra in sorted(invalid_extras_to_warn):
515-
logger.warning(
516-
"%s %s does not provide the extra '%s'",
517-
self.base.name,
518-
self.version,
519-
extra,
520-
)
521-
522-
def _calculate_valid_requested_extras(self) -> FrozenSet[str]:
523-
"""Get a list of valid extras requested by this candidate.
524-
525-
The user (or upstream dependant) may have specified extras that the
526-
candidate doesn't support. Any unsupported extras are dropped, and each
527-
cause a warning to be logged here.
528-
"""
529-
requested_extras = self.extras
530-
valid_extras = frozenset(
531-
extra
532-
for extra in requested_extras
533-
if self.base.dist.is_extra_provided(extra)
534-
)
535-
self._warn_invalid_extras(requested_extras, valid_extras)
536-
return valid_extras
537-
538494
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
539495
factory = self.base._factory
540496

@@ -544,7 +500,18 @@ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requiremen
544500
if not with_requires:
545501
return
546502

547-
valid_extras = self._calculate_valid_requested_extras()
503+
# The user may have specified extras that the candidate doesn't
504+
# support. We ignore any unsupported extras here.
505+
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
506+
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
507+
for extra in sorted(invalid_extras):
508+
logger.warning(
509+
"%s %s does not provide the extra '%s'",
510+
self.base.name,
511+
self.version,
512+
extra,
513+
)
514+
548515
for r in self.base.dist.iter_dependencies(valid_extras):
549516
yield from factory.make_requirements_from_spec(
550517
str(r),

0 commit comments

Comments
 (0)