4 from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast
6 from pip._vendor.packaging.utils import canonicalize_name
7 from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
8 from pip._vendor.resolvelib import Resolver as RLResolver
9 from pip._vendor.resolvelib.structs import DirectedGraph
11 from pip._internal.cache import WheelCache
12 from pip._internal.index.package_finder import PackageFinder
13 from pip._internal.operations.prepare import RequirementPreparer
14 from pip._internal.req.req_install import InstallRequirement
15 from pip._internal.req.req_set import RequirementSet
16 from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
17 from pip._internal.resolution.resolvelib.provider import PipProvider
18 from pip._internal.resolution.resolvelib.reporter import (
23 from .base import Candidate, Requirement
24 from .factory import Factory
27 from pip._vendor.resolvelib.resolvers import Result as RLResult
29 Result = RLResult[Requirement, Candidate, str]
32 logger = logging.getLogger(__name__)
35 class Resolver(BaseResolver):
36 _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
40 preparer: RequirementPreparer,
41 finder: PackageFinder,
42 wheel_cache: Optional[WheelCache],
43 make_install_req: InstallRequirementProvider,
45 ignore_dependencies: bool,
46 ignore_installed: bool,
47 ignore_requires_python: bool,
48 force_reinstall: bool,
49 upgrade_strategy: str,
50 py_version_info: Optional[Tuple[int, ...]] = None,
53 assert upgrade_strategy in self._allowed_strategies
55 self.factory = Factory(
58 make_install_req=make_install_req,
59 wheel_cache=wheel_cache,
60 use_user_site=use_user_site,
61 force_reinstall=force_reinstall,
62 ignore_installed=ignore_installed,
63 ignore_requires_python=ignore_requires_python,
64 py_version_info=py_version_info,
66 self.ignore_dependencies = ignore_dependencies
67 self.upgrade_strategy = upgrade_strategy
68 self._result: Optional[Result] = None
71 self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
73 collected = self.factory.collect_root_requirements(root_reqs)
74 provider = PipProvider(
76 constraints=collected.constraints,
77 ignore_dependencies=self.ignore_dependencies,
78 upgrade_strategy=self.upgrade_strategy,
79 user_requested=collected.user_requested,
81 if "PIP_RESOLVER_DEBUG" in os.environ:
82 reporter: BaseReporter = PipDebuggingReporter()
84 reporter = PipReporter()
85 resolver: RLResolver[Requirement, Candidate, str] = RLResolver(
91 try_to_avoid_resolution_too_deep = 2000000
92 result = self._result = resolver.resolve(
93 collected.requirements, max_rounds=try_to_avoid_resolution_too_deep
96 except ResolutionImpossible as e:
97 error = self.factory.get_installation_error(
98 cast("ResolutionImpossible[Requirement, Candidate]", e),
99 collected.constraints,
103 req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
104 for candidate in result.mapping.values():
105 ireq = candidate.get_install_requirement()
109 # Check if there is already an installation under the same name,
110 # and set a flag for later stages to uninstall it, if needed.
111 installed_dist = self.factory.get_dist_to_uninstall(candidate)
112 if installed_dist is None:
113 # There is no existing installation -- nothing to uninstall.
114 ireq.should_reinstall = False
115 elif self.factory.force_reinstall:
116 # The --force-reinstall flag is set -- reinstall.
117 ireq.should_reinstall = True
118 elif installed_dist.version != candidate.version:
119 # The installation is different in version -- reinstall.
120 ireq.should_reinstall = True
121 elif candidate.is_editable or installed_dist.editable:
122 # The incoming distribution is editable, or different in
123 # editable-ness to installation -- reinstall.
124 ireq.should_reinstall = True
125 elif candidate.source_link and candidate.source_link.is_file:
126 # The incoming distribution is under file://
127 if candidate.source_link.is_wheel:
128 # is a local wheel -- do nothing.
130 "%s is already installed with the same version as the "
131 "provided wheel. Use --force-reinstall to force an "
132 "installation of the wheel.",
137 # is a local sdist or path -- reinstall
138 ireq.should_reinstall = True
142 link = candidate.source_link
143 if link and link.is_yanked:
144 # The reason can contain non-ASCII characters, Unicode
145 # is required for Python 2.
147 "The candidate selected for download or install is a "
148 "yanked version: {name!r} candidate (version {version} "
149 "at {link})\nReason for being yanked: {reason}"
152 version=candidate.version,
154 reason=link.yanked_reason or "<none given>",
158 req_set.add_named_requirement(ireq)
160 reqs = req_set.all_requirements
161 self.factory.preparer.prepare_linked_requirements_more(reqs)
164 def get_installation_order(
165 self, req_set: RequirementSet
166 ) -> List[InstallRequirement]:
167 """Get order for installation of requirements in RequirementSet.
169 The returned list contains a requirement before another that depends on
170 it. This helps ensure that the environment is kept consistent as they
171 get installed one-by-one.
173 The current implementation creates a topological ordering of the
174 dependency graph, giving more weight to packages with less
175 or no dependencies, while breaking any cycles in the graph at
176 arbitrary points. We make no guarantees about where the cycle
177 would be broken, other than it *would* be broken.
179 assert self._result is not None, "must call resolve() first"
181 if not req_set.requirements:
182 # Nothing is left to install, so we do not need an order.
185 graph = self._result.graph
186 weights = get_topological_weights(graph, set(req_set.requirements.keys()))
188 sorted_items = sorted(
189 req_set.requirements.items(),
190 key=functools.partial(_req_set_item_sorter, weights=weights),
193 return [ireq for _, ireq in sorted_items]
196 def get_topological_weights(
197 graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
198 ) -> Dict[Optional[str], int]:
199 """Assign weights to each node based on how "deep" they are.
201 This implementation may change at any point in the future without prior
204 We first simplify the dependency graph by pruning any leaves and giving them
205 the highest weight: a package without any dependencies should be installed
206 first. This is done again and again in the same way, giving ever less weight
207 to the newly found leaves. The loop stops when no leaves are left: all
208 remaining packages have at least one dependency left in the graph.
210 Then we continue with the remaining graph, by taking the length for the
211 longest path to any node from root, ignoring any paths that contain a single
212 node twice (i.e. cycles). This is done through a depth-first search through
213 the graph, while keeping track of the path to the node.
215 Cycles in the graph result would result in node being revisited while also
216 being on its own path. In this case, take no action. This helps ensure we
217 don't get stuck in a cycle.
219 When assigning weight, the longer path (i.e. larger length) is preferred.
221 We are only interested in the weights of packages that are in the
224 path: Set[Optional[str]] = set()
225 weights: Dict[Optional[str], int] = {}
227 def visit(node: Optional[str]) -> None:
229 # We hit a cycle, so we'll break it here.
232 # Time to visit the children!
234 for child in graph.iter_children(node):
238 if node not in requirement_keys:
241 last_known_parent_count = weights.get(node, 0)
242 weights[node] = max(last_known_parent_count, len(path))
244 # Simplify the graph, pruning leaves that have no dependencies.
245 # This is needed for large graphs (say over 200 packages) because the
246 # `visit` function is exponentially slower then, taking minutes.
247 # See https://github.com/pypa/pip/issues/10557
248 # We will loop until we explicitly break the loop.
254 for _child in graph.iter_children(key):
255 # This means we have at least one child
261 # We are done simplifying.
263 # Calculate the weight for the leaves.
264 weight = len(graph) - 1
266 if leaf not in requirement_keys:
268 weights[leaf] = weight
269 # Remove the leaves from the graph, making it simpler.
273 # Visit the remaining graph.
274 # `None` is guaranteed to be the root node by resolvelib.
277 # Sanity check: all requirement keys should be in the weights,
278 # and no other keys should be in the weights.
279 difference = set(weights.keys()).difference(requirement_keys)
280 assert not difference, difference
285 def _req_set_item_sorter(
286 item: Tuple[str, InstallRequirement],
287 weights: Dict[Optional[str], int],
288 ) -> Tuple[int, str]:
289 """Key function used to sort install requirements for installation.
291 Based on the "weight" mapping calculated in ``get_installation_order()``.
292 The canonical package name is returned as the second member as a tie-
293 breaker to ensure the result is predictable, which is useful in tests.
295 name = canonicalize_name(item[0])
296 return weights[name], name