Skip to content

Pull in typing information from ResolveLib #9699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/resolvelib.vendor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgrade vendored resolvelib to 0.5.5.
11 changes: 8 additions & 3 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
Set,
Tuple,
TypeVar,
cast,
)

from pip._vendor.packaging.specifiers import SpecifierSet
Expand Down Expand Up @@ -419,7 +420,7 @@ def _report_requires_python_error(self, causes):
return UnsupportedPythonVersion(message)

def _report_single_requirement_conflict(self, req, parent):
# type: (Requirement, Candidate) -> DistributionNotFound
# type: (Requirement, Optional[Candidate]) -> DistributionNotFound
if parent is None:
req_disp = str(req)
else:
Expand All @@ -439,7 +440,7 @@ def _report_single_requirement_conflict(self, req, parent):

def get_installation_error(
self,
e, # type: ResolutionImpossible
e, # type: ResolutionImpossible[Requirement, Candidate]
constraints, # type: Dict[str, Constraint]
):
# type: (...) -> InstallationError
Expand All @@ -455,7 +456,11 @@ def get_installation_error(
and not cause.requirement.is_satisfied_by(self._python_candidate)
]
if requires_python_causes:
return self._report_requires_python_error(requires_python_causes)
# The comprehension above makes sure all Requirement instances are
# RequiresPythonRequirement, so let's cast for convinience.
return self._report_requires_python_error(
cast("Sequence[ConflictCause]", requires_python_causes),
)

# Otherwise, we have a set of causes which can't all be satisfied
# at once.
Expand Down
24 changes: 17 additions & 7 deletions src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union
from typing import TYPE_CHECKING, Dict, Iterable, Optional, Sequence, Union

from pip._vendor.resolvelib.providers import AbstractProvider

from .base import Candidate, Constraint, Requirement
from .factory import Factory

if TYPE_CHECKING:
from pip._vendor.resolvelib.providers import Preference
from pip._vendor.resolvelib.resolvers import RequirementInformation

PreferenceInformation = RequirementInformation[Requirement, Candidate]

_ProviderBase = AbstractProvider[Requirement, Candidate, str]
else:
_ProviderBase = AbstractProvider

# Notes on the relationship between the provider, the factory, and the
# candidate and requirement classes.
#
Expand All @@ -24,7 +34,7 @@
# services to those objects (access to pip's finder and preparer).


class PipProvider(AbstractProvider):
class PipProvider(_ProviderBase):
"""Pip's provider implementation for resolvelib.

:params constraints: A mapping of constraints specified by the user. Keys
Expand All @@ -50,17 +60,17 @@ def __init__(
self._upgrade_strategy = upgrade_strategy
self._user_requested = user_requested

def identify(self, dependency):
def identify(self, requirement_or_candidate):
# type: (Union[Requirement, Candidate]) -> str
return dependency.name
return requirement_or_candidate.name

def get_preference(
self,
resolution, # type: Optional[Candidate]
candidates, # type: Sequence[Candidate]
information, # type: Sequence[Tuple[Requirement, Candidate]]
candidates, # type: Iterable[Candidate]
information, # type: Iterable[PreferenceInformation]
):
# type: (...) -> Any
# type: (...) -> Preference
"""Produce a sort key for given requirement based on preference.

The lower the return value is, the more preferred this group of
Expand Down
25 changes: 17 additions & 8 deletions src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import functools
import logging
import os
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast

from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
from pip._vendor.resolvelib import Resolver as RLResolver
from pip._vendor.resolvelib.resolvers import Result
from pip._vendor.resolvelib.structs import DirectedGraph

from pip._internal.cache import WheelCache
from pip._internal.exceptions import InstallationError
Expand All @@ -28,11 +28,14 @@
from pip._internal.utils.filetypes import is_archive_file
from pip._internal.utils.misc import dist_is_editable

from .base import Constraint
from .base import Candidate, Constraint, Requirement
from .factory import Factory

if TYPE_CHECKING:
from pip._vendor.resolvelib.structs import DirectedGraph
from pip._vendor.resolvelib.resolvers import Result as RLResult

Result = RLResult[Requirement, Candidate, str]


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -114,7 +117,10 @@ def resolve(self, root_reqs, check_supported_wheels):
reporter = PipDebuggingReporter() # type: BaseReporter
else:
reporter = PipReporter()
resolver = RLResolver(provider, reporter)
resolver = RLResolver(
provider,
reporter,
) # type: RLResolver[Requirement, Candidate, str]

try:
try_to_avoid_resolution_too_deep = 2000000
Expand All @@ -123,7 +129,10 @@ def resolve(self, root_reqs, check_supported_wheels):
)

except ResolutionImpossible as e:
error = self.factory.get_installation_error(e, constraints)
error = self.factory.get_installation_error(
cast("ResolutionImpossible[Requirement, Candidate]", e),
constraints,
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to handle the type of e? ResolutionImpossible is a generic exception, but I’m not sure how to specify the type variables here.

Copy link
Member

@pradyunsg pradyunsg Apr 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, this is fine.

raise error from e

req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
Expand All @@ -148,7 +157,7 @@ def resolve(self, root_reqs, check_supported_wheels):
# The incoming distribution is editable, or different in
# editable-ness to installation -- reinstall.
ireq.should_reinstall = True
elif candidate.source_link.is_file:
elif candidate.source_link and candidate.source_link.is_file:
# The incoming distribution is under file://
if candidate.source_link.is_wheel:
# is a local wheel -- do nothing.
Expand Down Expand Up @@ -236,7 +245,7 @@ def get_installation_order(self, req_set):


def get_topological_weights(graph, expected_node_count):
# type: (DirectedGraph, int) -> Dict[Optional[str], int]
# type: (DirectedGraph[Optional[str]], int) -> Dict[Optional[str], int]
"""Assign weights to each node based on how "deep" they are.

This implementation may change at any point in the future without prior
Expand Down
1 change: 0 additions & 1 deletion src/pip/_vendor/resolvelib.pyi

This file was deleted.

2 changes: 1 addition & 1 deletion src/pip/_vendor/resolvelib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"ResolutionTooDeep",
]

__version__ = "0.5.4"
__version__ = "0.5.5"


from .providers import AbstractProvider, AbstractResolver
Expand Down
15 changes: 15 additions & 0 deletions src/pip/_vendor/resolvelib/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
__version__: str

from .providers import (
AbstractResolver as AbstractResolver,
AbstractProvider as AbstractProvider,
)
from .reporters import BaseReporter as BaseReporter
from .resolvers import (
InconsistentCandidate as InconsistentCandidate,
RequirementsConflicted as RequirementsConflicted,
Resolver as Resolver,
ResolutionError as ResolutionError,
ResolutionImpossible as ResolutionImpossible,
ResolutionTooDeep as ResolutionTooDeep,
)
8 changes: 3 additions & 5 deletions src/pip/_vendor/resolvelib/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ class AbstractProvider(object):
"""Delegate class to provide requirement interface for the resolver."""

def identify(self, requirement_or_candidate):
"""Given a requirement or candidate, return an identifier for it.
"""Given a requirement, return an identifier for it.

This is used in many places to identify a requirement or candidate,
e.g. whether two requirements should have their specifier parts merged,
whether two candidates would conflict with each other (because they
have same name but different versions).
This is used to identify a requirement, e.g. whether two requirements
should have their specifier parts merged.
"""
raise NotImplementedError

Expand Down
44 changes: 44 additions & 0 deletions src/pip/_vendor/resolvelib/providers.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from typing import (
Any,
Collection,
Generic,
Iterable,
Mapping,
Optional,
Protocol,
Sequence,
Union,
)

from .reporters import BaseReporter
from .resolvers import RequirementInformation
from .structs import (
KT,
RT,
CT,
IterableView,
Matches,
)

class Preference(Protocol):
def __lt__(self, __other: Any) -> bool: ...

class AbstractProvider(Generic[RT, CT, KT]):
def identify(self, requirement_or_candidate: Union[RT, CT]) -> KT: ...
def get_preference(
self,
resolution: Optional[CT],
candidates: IterableView[CT],
information: Collection[RequirementInformation[RT, CT]],
) -> Preference: ...
def find_matches(self, requirements: Sequence[RT]) -> Matches: ...
def is_satisfied_by(self, requirement: RT, candidate: CT) -> bool: ...
def get_dependencies(self, candidate: CT) -> Iterable[RT]: ...

class AbstractResolver(Generic[RT, CT, KT]):
base_exception = Exception
provider: AbstractProvider[RT, CT, KT]
reporter: BaseReporter
def __init__(
self, provider: AbstractProvider[RT, CT, KT], reporter: BaseReporter
): ...
Empty file.
10 changes: 10 additions & 0 deletions src/pip/_vendor/resolvelib/reporters.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Any

class BaseReporter:
def starting(self) -> Any: ...
def starting_round(self, index: int) -> Any: ...
def ending_round(self, index: int, state: Any) -> Any: ...
def ending(self, state: Any) -> Any: ...
def adding_requirement(self, requirement: Any, parent: Any) -> Any: ...
def backtracking(self, candidate: Any) -> Any: ...
def pinning(self, candidate: Any) -> Any: ...
39 changes: 20 additions & 19 deletions src/pip/_vendor/resolvelib/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ def __repr__(self):
@classmethod
def from_requirement(cls, provider, requirement, parent):
"""Build an instance from a requirement."""
cands = build_iter_view(provider.find_matches([requirement]))
matches = provider.find_matches(requirements=[requirement])
cands = build_iter_view(matches)
infos = [RequirementInformation(requirement, parent)]
criterion = cls(cands, infos, incompatibilities=[])
if not cands:
Expand All @@ -93,7 +94,8 @@ def merged_with(self, provider, requirement, parent):
"""Build a new instance from this and a new requirement."""
infos = list(self.information)
infos.append(RequirementInformation(requirement, parent))
cands = build_iter_view(provider.find_matches([r for r, _ in infos]))
matches = provider.find_matches([r for r, _ in infos])
cands = build_iter_view(matches)
criterion = type(self)(cands, infos, list(self.incompatibilities))
if not cands:
raise RequirementsConflicted(criterion)
Expand Down Expand Up @@ -165,22 +167,21 @@ def _push_new_state(self):
self._states.append(state)

def _merge_into_criterion(self, requirement, parent):
self._r.adding_requirement(requirement, parent)
name = self._p.identify(requirement)
try:
self._r.adding_requirement(requirement=requirement, parent=parent)
name = self._p.identify(requirement_or_candidate=requirement)
if name in self.state.criteria:
crit = self.state.criteria[name]
except KeyError:
crit = Criterion.from_requirement(self._p, requirement, parent)
else:
crit = crit.merged_with(self._p, requirement, parent)
else:
crit = Criterion.from_requirement(self._p, requirement, parent)
return name, crit

def _get_criterion_item_preference(self, item):
name, criterion = item
return self._p.get_preference(
self.state.mapping.get(name),
criterion.candidates.for_preference(),
criterion.information,
resolution=self.state.mapping.get(name),
candidates=criterion.candidates.for_preference(),
information=criterion.information,
)

def _is_current_pin_satisfying(self, name, criterion):
Expand All @@ -189,13 +190,13 @@ def _is_current_pin_satisfying(self, name, criterion):
except KeyError:
return False
return all(
self._p.is_satisfied_by(r, current_pin)
self._p.is_satisfied_by(requirement=r, candidate=current_pin)
for r in criterion.iter_requirement()
)

def _get_criteria_to_update(self, candidate):
criteria = {}
for r in self._p.get_dependencies(candidate):
for r in self._p.get_dependencies(candidate=candidate):
name, crit = self._merge_into_criterion(r, parent=candidate)
criteria[name] = crit
return criteria
Expand All @@ -214,15 +215,15 @@ def _attempt_to_pin_criterion(self, name, criterion):
# faulty provider, we will raise an error to notify the implementer
# to fix find_matches() and/or is_satisfied_by().
satisfied = all(
self._p.is_satisfied_by(r, candidate)
self._p.is_satisfied_by(requirement=r, candidate=candidate)
for r in criterion.iter_requirement()
)
if not satisfied:
raise InconsistentCandidate(candidate, criterion)

# Put newly-pinned candidate at the end. This is essential because
# backtracking looks at this mapping to get the last pin.
self._r.pinning(candidate)
self._r.pinning(candidate=candidate)
self.state.mapping.pop(name, None)
self.state.mapping[name] = candidate
self.state.criteria.update(criteria)
Expand Down Expand Up @@ -274,7 +275,7 @@ def _backtrack(self):
# Also mark the newly known incompatibility.
incompatibilities_from_broken.append((name, [candidate]))

self._r.backtracking(candidate)
self._r.backtracking(candidate=candidate)

# Create a new state from the last known-to-work one, and apply
# the previously gathered incompatibility information.
Expand Down Expand Up @@ -326,7 +327,7 @@ def resolve(self, requirements, max_rounds):
self._push_new_state()

for round_index in range(max_rounds):
self._r.starting_round(round_index)
self._r.starting_round(index=round_index)

unsatisfied_criterion_items = [
item
Expand All @@ -336,7 +337,7 @@ def resolve(self, requirements, max_rounds):

# All criteria are accounted for. Nothing more to pin, we are done!
if not unsatisfied_criterion_items:
self._r.ending(self.state)
self._r.ending(state=self.state)
return self.state

# Choose the most preferred unpinned criterion to try.
Expand All @@ -359,7 +360,7 @@ def resolve(self, requirements, max_rounds):
# Pinning was successful. Push a new state to do another pin.
self._push_new_state()

self._r.ending_round(round_index, self.state)
self._r.ending_round(index=round_index, state=self.state)

raise ResolutionTooDeep(max_rounds)

Expand Down
Loading