Skip to content

Commit d64190c

Browse files
committed
Try to find dependencies from unnormalized extras
When an unnormalized extra is requested, try to look up dependencies with both its raw and normalized forms, to maintain compatibility when an extra is both specified and requested in a non-standard form.
1 parent b9066d4 commit d64190c

File tree

2 files changed

+57
-23
lines changed

2 files changed

+57
-23
lines changed

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

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,17 @@ class ExtrasCandidate(Candidate):
423423
def __init__(
424424
self,
425425
base: BaseCandidate,
426-
extras: FrozenSet[NormalizedName],
426+
extras: FrozenSet[str],
427427
) -> None:
428428
self.base = base
429-
self.extras = extras
429+
self.extras = frozenset(canonicalize_name(e) for e in extras)
430+
# If any extras are requested in their non-normalized forms, keep track
431+
# of their raw values. This is needed when we look up dependencies
432+
# since PEP 685 has not been implemented for marker-matching, and using
433+
# the non-normalized extra for lookup ensures the user can select a
434+
# non-normalized extra in a package with its non-normalized form.
435+
# TODO: Remove this when packaging is upgraded to support PEP 685.
436+
self._unnormalized_extras = extras.difference(self.extras)
430437

431438
def __str__(self) -> str:
432439
name, rest = str(self.base).split(" ", 1)
@@ -477,6 +484,44 @@ def is_editable(self) -> bool:
477484
def source_link(self) -> Optional[Link]:
478485
return self.base.source_link
479486

487+
def _warn_invalid_extras(
488+
self,
489+
requested: FrozenSet[str],
490+
provided: FrozenSet[str],
491+
) -> None:
492+
"""Emit warnings for invalid extras being requested.
493+
494+
This emits a warning for each requested extra that is not in the
495+
candidate's ``Provides-Extra`` list.
496+
"""
497+
invalid_extras_to_warn = requested.difference(
498+
provided,
499+
# If an extra is requested in an unnormalized form, skip warning
500+
# about the normalized form being missing.
501+
(canonicalize_name(e) for e in self._unnormalized_extras),
502+
)
503+
if not invalid_extras_to_warn:
504+
return
505+
for extra in sorted(invalid_extras_to_warn):
506+
logger.warning(
507+
"%s %s does not provide the extra '%s'",
508+
self.base.name,
509+
self.version,
510+
extra,
511+
)
512+
513+
def _calculate_valid_requested_extras(self) -> FrozenSet[str]:
514+
"""Get a list of valid extras requested by this candidate.
515+
516+
The user (or upstream dependant) may have specified extras that the
517+
candidate doesn't support. Any unsupported extras are dropped, and each
518+
cause a warning to be logged here.
519+
"""
520+
requested_extras = self.extras.union(self._unnormalized_extras)
521+
provided_extras = frozenset(self.base.dist.iter_provided_extras())
522+
self._warn_invalid_extras(requested_extras, provided_extras)
523+
return requested_extras.intersection(provided_extras)
524+
480525
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
481526
factory = self.base._factory
482527

@@ -486,18 +531,7 @@ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requiremen
486531
if not with_requires:
487532
return
488533

489-
# The user may have specified extras that the candidate doesn't
490-
# support. We ignore any unsupported extras here.
491-
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
492-
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
493-
for extra in sorted(invalid_extras):
494-
logger.warning(
495-
"%s %s does not provide the extra '%s'",
496-
self.base.name,
497-
self.version,
498-
extra,
499-
)
500-
534+
valid_extras = self._calculate_valid_requested_extras()
501535
for r in self.base.dist.iter_dependencies(valid_extras):
502536
requirement = factory.make_requirement_from_spec(
503537
str(r), self.base._ireq, valid_extras

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None:
140140
def _make_extras_candidate(
141141
self,
142142
base: BaseCandidate,
143-
extras: FrozenSet[NormalizedName],
143+
extras: FrozenSet[str],
144144
) -> ExtrasCandidate:
145-
cache_key = (id(base), extras)
145+
cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
146146
try:
147147
candidate = self._extras_candidate_cache[cache_key]
148148
except KeyError:
@@ -153,7 +153,7 @@ def _make_extras_candidate(
153153
def _make_candidate_from_dist(
154154
self,
155155
dist: BaseDistribution,
156-
extras: FrozenSet[NormalizedName],
156+
extras: FrozenSet[str],
157157
template: InstallRequirement,
158158
) -> Candidate:
159159
try:
@@ -168,7 +168,7 @@ def _make_candidate_from_dist(
168168
def _make_candidate_from_link(
169169
self,
170170
link: Link,
171-
extras: FrozenSet[NormalizedName],
171+
extras: FrozenSet[str],
172172
template: InstallRequirement,
173173
name: Optional[NormalizedName],
174174
version: Optional[CandidateVersion],
@@ -246,12 +246,12 @@ def _iter_found_candidates(
246246
assert template.req, "Candidates found on index must be PEP 508"
247247
name = canonicalize_name(template.req.name)
248248

249-
extras: FrozenSet[NormalizedName] = frozenset()
249+
extras: FrozenSet[str] = frozenset()
250250
for ireq in ireqs:
251251
assert ireq.req, "Candidates found on index must be PEP 508"
252252
specifier &= ireq.req.specifier
253253
hashes &= ireq.hashes(trust_internet=False)
254-
extras |= frozenset(canonicalize_name(e) for e in ireq.extras)
254+
extras |= frozenset(ireq.extras)
255255

256256
def _get_installed_candidate() -> Optional[Candidate]:
257257
"""Get the candidate for the currently-installed version."""
@@ -327,7 +327,7 @@ def is_pinned(specifier: SpecifierSet) -> bool:
327327
def _iter_explicit_candidates_from_base(
328328
self,
329329
base_requirements: Iterable[Requirement],
330-
extras: FrozenSet[NormalizedName],
330+
extras: FrozenSet[str],
331331
) -> Iterator[Candidate]:
332332
"""Produce explicit candidates from the base given an extra-ed package.
333333
@@ -394,7 +394,7 @@ def find_candidates(
394394
explicit_candidates.update(
395395
self._iter_explicit_candidates_from_base(
396396
requirements.get(parsed_requirement.name, ()),
397-
frozenset(canonicalize_name(e) for e in parsed_requirement.extras),
397+
frozenset(parsed_requirement.extras),
398398
),
399399
)
400400

@@ -454,7 +454,7 @@ def _make_requirement_from_install_req(
454454
self._fail_if_link_is_unsupported_wheel(ireq.link)
455455
cand = self._make_candidate_from_link(
456456
ireq.link,
457-
extras=frozenset(canonicalize_name(e) for e in ireq.extras),
457+
extras=frozenset(ireq.extras),
458458
template=ireq,
459459
name=canonicalize_name(ireq.name) if ireq.name else None,
460460
version=None,

0 commit comments

Comments
 (0)