Skip to content

Commit

Permalink
Try to find dependencies from unnormalized extras
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
uranusjr committed May 11, 2023
1 parent b9066d4 commit d64190c
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 23 deletions.
62 changes: 48 additions & 14 deletions src/pip/_internal/resolution/resolvelib/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,17 @@ class ExtrasCandidate(Candidate):
def __init__(
self,
base: BaseCandidate,
extras: FrozenSet[NormalizedName],
extras: FrozenSet[str],
) -> None:
self.base = base
self.extras = extras
self.extras = frozenset(canonicalize_name(e) for e in extras)
# If any extras are requested in their non-normalized forms, keep track
# of their raw values. This is needed when we look up dependencies
# since PEP 685 has not been implemented for marker-matching, and using
# the non-normalized extra for lookup ensures the user can select a
# non-normalized extra in a package with its non-normalized form.
# TODO: Remove this when packaging is upgraded to support PEP 685.
self._unnormalized_extras = extras.difference(self.extras)

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

def _warn_invalid_extras(
self,
requested: FrozenSet[str],
provided: FrozenSet[str],
) -> None:
"""Emit warnings for invalid extras being requested.
This emits a warning for each requested extra that is not in the
candidate's ``Provides-Extra`` list.
"""
invalid_extras_to_warn = requested.difference(
provided,
# If an extra is requested in an unnormalized form, skip warning
# about the normalized form being missing.
(canonicalize_name(e) for e in self._unnormalized_extras),
)
if not invalid_extras_to_warn:
return
for extra in sorted(invalid_extras_to_warn):
logger.warning(
"%s %s does not provide the extra '%s'",
self.base.name,
self.version,
extra,
)

def _calculate_valid_requested_extras(self) -> FrozenSet[str]:
"""Get a list of valid extras requested by this candidate.
The user (or upstream dependant) may have specified extras that the
candidate doesn't support. Any unsupported extras are dropped, and each
cause a warning to be logged here.
"""
requested_extras = self.extras.union(self._unnormalized_extras)
provided_extras = frozenset(self.base.dist.iter_provided_extras())
self._warn_invalid_extras(requested_extras, provided_extras)
return requested_extras.intersection(provided_extras)

def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
factory = self.base._factory

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

# The user may have specified extras that the candidate doesn't
# support. We ignore any unsupported extras here.
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
for extra in sorted(invalid_extras):
logger.warning(
"%s %s does not provide the extra '%s'",
self.base.name,
self.version,
extra,
)

valid_extras = self._calculate_valid_requested_extras()
for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras
Expand Down
18 changes: 9 additions & 9 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None:
def _make_extras_candidate(
self,
base: BaseCandidate,
extras: FrozenSet[NormalizedName],
extras: FrozenSet[str],
) -> ExtrasCandidate:
cache_key = (id(base), extras)
cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
try:
candidate = self._extras_candidate_cache[cache_key]
except KeyError:
Expand All @@ -153,7 +153,7 @@ def _make_extras_candidate(
def _make_candidate_from_dist(
self,
dist: BaseDistribution,
extras: FrozenSet[NormalizedName],
extras: FrozenSet[str],
template: InstallRequirement,
) -> Candidate:
try:
Expand All @@ -168,7 +168,7 @@ def _make_candidate_from_dist(
def _make_candidate_from_link(
self,
link: Link,
extras: FrozenSet[NormalizedName],
extras: FrozenSet[str],
template: InstallRequirement,
name: Optional[NormalizedName],
version: Optional[CandidateVersion],
Expand Down Expand Up @@ -246,12 +246,12 @@ def _iter_found_candidates(
assert template.req, "Candidates found on index must be PEP 508"
name = canonicalize_name(template.req.name)

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

def _get_installed_candidate() -> Optional[Candidate]:
"""Get the candidate for the currently-installed version."""
Expand Down Expand Up @@ -327,7 +327,7 @@ def is_pinned(specifier: SpecifierSet) -> bool:
def _iter_explicit_candidates_from_base(
self,
base_requirements: Iterable[Requirement],
extras: FrozenSet[NormalizedName],
extras: FrozenSet[str],
) -> Iterator[Candidate]:
"""Produce explicit candidates from the base given an extra-ed package.
Expand Down Expand Up @@ -394,7 +394,7 @@ def find_candidates(
explicit_candidates.update(
self._iter_explicit_candidates_from_base(
requirements.get(parsed_requirement.name, ()),
frozenset(canonicalize_name(e) for e in parsed_requirement.extras),
frozenset(parsed_requirement.extras),
),
)

Expand Down Expand Up @@ -454,7 +454,7 @@ def _make_requirement_from_install_req(
self._fail_if_link_is_unsupported_wheel(ireq.link)
cand = self._make_candidate_from_link(
ireq.link,
extras=frozenset(canonicalize_name(e) for e in ireq.extras),
extras=frozenset(ireq.extras),
template=ireq,
name=canonicalize_name(ireq.name) if ireq.name else None,
version=None,
Expand Down

0 comments on commit d64190c

Please sign in to comment.