Let us walk on the 3-isogeny graph
Loading...
Searching...
No Matches
resolver.py
Go to the documentation of this file.
1"""Dependency Resolution
2
3The dependency resolution in pip is performed as follows:
4
5for top-level requirements:
6 a. only one spec allowed per project, regardless of conflicts or not.
7 otherwise a "double requirement" exception is raised
8 b. they override sub-dependency requirements.
9for sub-dependencies
10 a. "first found, wins" (where the order is breadth first)
11"""
12
13# The following comment should be removed at some point in the future.
14# mypy: strict-optional=False
15
16import logging
17import sys
18from collections import defaultdict
19from itertools import chain
20from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
21
22from pip._vendor.packaging import specifiers
23from pip._vendor.packaging.requirements import Requirement
24
25from pip._internal.cache import WheelCache
26from pip._internal.exceptions import (
27 BestVersionAlreadyInstalled,
28 DistributionNotFound,
29 HashError,
30 HashErrors,
31 InstallationError,
32 NoneMetadataError,
33 UnsupportedPythonVersion,
34)
35from pip._internal.index.package_finder import PackageFinder
36from pip._internal.metadata import BaseDistribution
37from pip._internal.models.link import Link
38from pip._internal.models.wheel import Wheel
39from pip._internal.operations.prepare import RequirementPreparer
41 InstallRequirement,
42 check_invalid_constraint_type,
43)
44from pip._internal.req.req_set import RequirementSet
45from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
46from pip._internal.utils import compatibility_tags
47from pip._internal.utils.compatibility_tags import get_supported
48from pip._internal.utils.direct_url_helpers import direct_url_from_link
49from pip._internal.utils.logging import indent_log
50from pip._internal.utils.misc import normalize_version_info
51from pip._internal.utils.packaging import check_requires_python
52
53logger = logging.getLogger(__name__)
54
55DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
56
57
59 dist: BaseDistribution,
60 version_info: Tuple[int, int, int],
61 ignore_requires_python: bool = False,
62) -> None:
63 """
64 Check whether the given Python version is compatible with a distribution's
65 "Requires-Python" value.
66
67 :param version_info: A 3-tuple of ints representing the Python
68 major-minor-micro version to check.
69 :param ignore_requires_python: Whether to ignore the "Requires-Python"
70 value if the given Python version isn't compatible.
71
72 :raises UnsupportedPythonVersion: When the given Python version isn't
73 compatible.
74 """
75 # This idiosyncratically converts the SpecifierSet to str and let
76 # check_requires_python then parse it again into SpecifierSet. But this
77 # is the legacy resolver so I'm just not going to bother refactoring.
78 try:
79 requires_python = str(dist.requires_python)
80 except FileNotFoundError as e:
81 raise NoneMetadataError(dist, str(e))
82 try:
83 is_compatible = check_requires_python(
84 requires_python,
85 version_info=version_info,
86 )
87 except specifiers.InvalidSpecifier as exc:
89 "Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
90 )
91 return
92
93 if is_compatible:
94 return
95
96 version = ".".join(map(str, version_info))
97 if ignore_requires_python:
99 "Ignoring failed Requires-Python check for package %r: %s not in %r",
101 version,
102 requires_python,
103 )
104 return
105
107 "Package {!r} requires a different Python: {} not in {!r}".format(
108 dist.raw_name, version, requires_python
109 )
110 )
111
112
114 """Resolves which packages need to be installed/uninstalled to perform \
115 the requested operation without breaking the requirements of any package.
116 """
117
118 _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
119
121 self,
122 preparer: RequirementPreparer,
123 finder: PackageFinder,
124 wheel_cache: Optional[WheelCache],
125 make_install_req: InstallRequirementProvider,
126 use_user_site: bool,
127 ignore_dependencies: bool,
128 ignore_installed: bool,
129 ignore_requires_python: bool,
130 force_reinstall: bool,
131 upgrade_strategy: str,
132 py_version_info: Optional[Tuple[int, ...]] = None,
133 ) -> None:
134 super().__init__()
135 assert upgrade_strategy in self._allowed_strategies
136
137 if py_version_info is None:
138 py_version_info = sys.version_info[:3]
139 else:
140 py_version_info = normalize_version_info(py_version_info)
141
142 self._py_version_info = py_version_info
143
144 self.preparer = preparer
145 self.finder = finder
146 self.wheel_cache = wheel_cache
147
148 self.upgrade_strategy = upgrade_strategy
149 self.force_reinstall = force_reinstall
150 self.ignore_dependencies = ignore_dependencies
151 self.ignore_installed = ignore_installed
152 self.ignore_requires_python = ignore_requires_python
153 self.use_user_site = use_user_site
154 self._make_install_req = make_install_req
155
156 self._discovered_dependencies: DiscoveredDependencies = defaultdict(list)
157
159 self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
160 ) -> RequirementSet:
161 """Resolve what operations need to be done
162
163 As a side-effect of this method, the packages (and their dependencies)
164 are downloaded, unpacked and prepared for installation. This
165 preparation is done by ``pip.operations.prepare``.
166
167 Once PyPI has static dependency metadata available, it would be
168 possible to move the preparation to become a step separated from
169 dependency resolution.
170 """
171 requirement_set = RequirementSet(check_supported_wheels=check_supported_wheels)
172 for req in root_reqs:
174 check_invalid_constraint_type(req)
175 self._add_requirement_to_set(requirement_set, req)
176
177 # Actually prepare the files, and collect any exceptions. Most hash
178 # exceptions cannot be checked ahead of time, because
179 # _populate_link() needs to be called before we can make decisions
180 # based on link type.
181 discovered_reqs: List[InstallRequirement] = []
182 hash_errors = HashErrors()
183 for req in chain(requirement_set.all_requirements, discovered_reqs):
184 try:
185 discovered_reqs.extend(self._resolve_one(requirement_set, req))
186 except HashError as exc:
187 exc.req = req
189
190 if hash_errors:
191 raise hash_errors
192
193 return requirement_set
194
196 self,
197 requirement_set: RequirementSet,
198 install_req: InstallRequirement,
199 parent_req_name: Optional[str] = None,
200 extras_requested: Optional[Iterable[str]] = None,
201 ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
202 """Add install_req as a requirement to install.
203
204 :param parent_req_name: The name of the requirement that needed this
205 added. The name is used because when multiple unnamed requirements
206 resolve to the same name, we could otherwise end up with dependency
207 links that point outside the Requirements set. parent_req must
208 already be added. Note that None implies that this is a user
209 supplied requirement, vs an inferred one.
210 :param extras_requested: an iterable of extras used to evaluate the
211 environment markers.
212 :return: Additional requirements to scan. That is either [] if
213 the requirement is not applicable, or [install_req] if the
214 requirement is applicable and has just been added.
215 """
216 # If the markers do not match, ignore this requirement.
217 if not install_req.match_markers(extras_requested):
219 "Ignoring %s: markers '%s' don't match your environment",
222 )
223 return [], None
224
225 # If the wheel is not supported, raise an error.
226 # Should check this after filtering out based on environment markers to
227 # allow specifying different wheels based on the environment/OS, in a
228 # single requirements file.
233 raise InstallationError(
234 "{} is not a supported wheel on this platform.".format(
236 )
237 )
238
239 # This next bit is really a sanity check.
240 assert (
241 not install_req.user_supplied or parent_req_name is None
242 ), "a user supplied req shouldn't have a parent"
243
244 # Unnamed requirements are scanned again and the requirement won't be
245 # added as a dependency until after scanning.
246 if not install_req.name:
248 return [install_req], None
249
250 try:
251 existing_req: Optional[
252 InstallRequirement
254 except KeyError:
255 existing_req = None
256
257 has_conflicting_requirement = (
258 parent_req_name is None
259 and existing_req
265 )
266 if has_conflicting_requirement:
267 raise InstallationError(
268 "Double requirement given: {} (already in {}, name={!r})".format(
269 install_req, existing_req, install_req.name
270 )
271 )
272
273 # When no existing requirement exists, add the requirement as a
274 # dependency and it will be scanned again after.
275 if not existing_req:
277 # We'd want to rescan this requirement later
278 return [install_req], install_req
279
280 # Assume there's no need to scan, and that we've already
281 # encountered this for scanning.
283 return [], existing_req
284
285 does_not_satisfy_constraint = install_req.link and not (
287 )
288 if does_not_satisfy_constraint:
289 raise InstallationError(
290 "Could not satisfy constraints for '{}': "
291 "installation from path or url cannot be "
292 "constrained to a version".format(install_req.name)
293 )
294 # If we're now installing a constraint, mark the existing
295 # object for real installation.
297 # If we're now installing a user supplied requirement,
298 # mark the existing object as such.
301 existing_req.extras = tuple(
302 sorted(set(existing_req.extras) | set(install_req.extras))
303 )
305 "Setting %s extras to: %s",
306 existing_req,
308 )
309 # Return the existing requirement for addition to the parent and
310 # scanning again.
311 return [existing_req], existing_req
312
313 def _is_upgrade_allowed(self, req: InstallRequirement) -> bool:
314 if self.upgrade_strategy == "to-satisfy-only":
315 return False
316 elif self.upgrade_strategy == "eager":
317 return True
318 else:
319 assert self.upgrade_strategy == "only-if-needed"
321
322 def _set_req_to_reinstall(self, req: InstallRequirement) -> None:
323 """
324 Set a requirement to be installed.
325 """
326 # Don't uninstall the conflict if doing a user install and the
327 # conflict is not a user install.
330 req.satisfied_by = None
331
333 self, req_to_install: InstallRequirement
334 ) -> Optional[str]:
335 """Check if req_to_install should be skipped.
336
337 This will check if the req is installed, and whether we should upgrade
338 or reinstall it, taking into account all the relevant user options.
339
340 After calling this req_to_install will only have satisfied_by set to
341 None if the req_to_install is to be upgraded/reinstalled etc. Any
342 other value will be a dist recording the current thing installed that
343 satisfies the requirement.
344
345 Note that for vcs urls and the like we can't assess skipping in this
346 routine - we simply identify that we need to pull the thing down,
347 then later on it is pulled down and introspected to assess upgrade/
348 reinstalls etc.
349
350 :return: A text reason for why it was skipped, or None.
351 """
352 if self.ignore_installed:
353 return None
354
357 return None
358
359 if self.force_reinstall:
360 self._set_req_to_reinstall(req_to_install)
361 return None
362
363 if not self._is_upgrade_allowed(req_to_install):
364 if self.upgrade_strategy == "only-if-needed":
365 return "already satisfied, skipping upgrade"
366 return "already satisfied"
367
368 # Check for the possibility of an upgrade. For link-based
369 # requirements we have to pull the tree down and inspect to assess
370 # the version #, so it's handled way down.
371 if not req_to_install.link:
372 try:
373 self.finder.find_requirement(req_to_install, upgrade=True)
374 except BestVersionAlreadyInstalled:
375 # Then the best version is installed.
376 return "already up-to-date"
377 except DistributionNotFound:
378 # No distribution found, so we squash the error. It will
379 # be raised later when we re-try later to do the install.
380 # Why don't we just raise here?
381 pass
382
383 self._set_req_to_reinstall(req_to_install)
384 return None
385
386 def _find_requirement_link(self, req: InstallRequirement) -> Optional[Link]:
387 upgrade = self._is_upgrade_allowed(req)
388 best_candidate = self.finder.find_requirement(req, upgrade)
389 if not best_candidate:
390 return None
391
392 # Log a warning per PEP 592 if necessary before returning.
395 reason = link.yanked_reason or "<none given>"
396 msg = (
397 # Mark this as a unicode string to prevent
398 # "UnicodeEncodeError: 'ascii' codec can't encode character"
399 # in Python 2 when the reason contains non-ascii characters.
400 "The candidate selected for download or install is a "
401 "yanked version: {candidate}\n"
402 "Reason for being yanked: {reason}"
403 ).format(candidate=best_candidate, reason=reason)
404 logger.warning(msg)
405
406 return link
407
408 def _populate_link(self, req: InstallRequirement) -> None:
409 """Ensure that if a link can be found for this, that it is found.
410
411 Note that req.link may still be None - if the requirement is already
412 installed and not needed to be upgraded based on the return value of
413 _is_upgrade_allowed().
414
415 If preparer.require_hashes is True, don't use the wheel cache, because
416 cached wheels, always built locally, have different hashes than the
417 files downloaded from the index server and thus throw false hash
418 mismatches. Furthermore, cached wheels at present have undeterministic
419 contents due to file modification times.
420 """
421 if req.link is None:
423
424 if self.wheel_cache is None or self.preparer.require_hashes:
425 return
426 cache_entry = self.wheel_cache.get_cache_entry(
427 link=req.link,
428 package_name=req.name,
429 supported_tags=get_supported(),
430 )
431 if cache_entry is not None:
432 logger.debug("Using cached wheel link: %s", cache_entry.link)
435 if cache_entry.origin is not None:
437 else:
438 # Legacy cache entry that does not have origin.json.
439 # download_info may miss the archive_info.hashes field.
440 req.download_info = direct_url_from_link(
441 req.link, link_is_in_wheel_cache=cache_entry.persistent
442 )
444
445 def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:
446 """Takes a InstallRequirement and returns a single AbstractDist \
447 representing a prepared variant of the same.
448 """
449 if req.editable:
450 return self.preparer.prepare_editable_requirement(req)
451
452 # satisfied_by is only evaluated by calling _check_skip_installed,
453 # so it must be None here.
454 assert req.satisfied_by is None
455 skip_reason = self._check_skip_installed(req)
456
458 return self.preparer.prepare_installed_requirement(req, skip_reason)
459
460 # We eagerly populate the link, since that's our "legacy" behavior.
461 self._populate_link(req)
462 dist = self.preparer.prepare_linked_requirement(req)
463
464 # NOTE
465 # The following portion is for determining if a certain package is
466 # going to be re-installed/upgraded or not and reporting to the user.
467 # This should probably get cleaned up in a future refactor.
468
469 # req.req is only avail after unpack for URL
470 # pkgs repeat check_if_exists to uninstall-on-upgrade
471 # (#14)
472 if not self.ignore_installed:
474
476 should_modify = (
477 self.upgrade_strategy != "to-satisfy-only"
478 or self.force_reinstall
479 or self.ignore_installed
480 or req.link.scheme == "file"
481 )
482 if should_modify:
483 self._set_req_to_reinstall(req)
484 else:
486 "Requirement already satisfied (use --upgrade to upgrade): %s",
487 req,
488 )
489 return dist
490
492 self,
493 requirement_set: RequirementSet,
494 req_to_install: InstallRequirement,
495 ) -> List[InstallRequirement]:
496 """Prepare a single requirements file.
497
498 :return: A list of additional InstallRequirements to also install.
499 """
500 # Tell user what we are doing for this requirement:
501 # obtain (editable), skipping, processing (local url), collecting
502 # (remote url or package name)
504 return []
505
507
508 # Parse and return dependencies
509 dist = self._get_dist_for(req_to_install)
510 # This will raise UnsupportedPythonVersion if the given Python
511 # version isn't compatible with the distribution's Requires-Python.
513 dist,
514 version_info=self._py_version_info,
515 ignore_requires_python=self.ignore_requires_python,
516 )
517
518 more_reqs: List[InstallRequirement] = []
519
520 def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
521 # This idiosyncratically converts the Requirement to str and let
522 # make_install_req then parse it again into Requirement. But this is
523 # the legacy resolver so I'm just not going to bother refactoring.
524 sub_install_req = self._make_install_req(str(subreq), req_to_install)
525 parent_req_name = req_to_install.name
526 to_scan_again, add_to_parent = self._add_requirement_to_set(
527 requirement_set,
528 sub_install_req,
529 parent_req_name=parent_req_name,
530 extras_requested=extras_requested,
531 )
532 if parent_req_name and add_to_parent:
533 self._discovered_dependencies[parent_req_name].append(add_to_parent)
534 more_reqs.extend(to_scan_again)
535
536 with indent_log():
537 # We add req_to_install before its dependencies, so that we
538 # can refer to it when adding dependencies.
540 # 'unnamed' requirements will get added here
541 # 'unnamed' requirements can only come from being directly
542 # provided by the user.
545 requirement_set, req_to_install, parent_req_name=None
546 )
547
548 if not self.ignore_dependencies:
551 "Installing extra requirements: %r",
552 ",".join(req_to_install.extras),
553 )
554 missing_requested = sorted(
556 )
557 for missing in missing_requested:
559 "%s %s does not provide the extra '%s'",
562 missing,
563 )
564
565 available_requested = sorted(
567 )
568 for subreq in dist.iter_dependencies(available_requested):
569 add_req(subreq, extras_requested=available_requested)
570
571 return more_reqs
572
574 self, req_set: RequirementSet
575 ) -> List[InstallRequirement]:
576 """Create the installation order.
577
578 The installation order is topological - requirements are installed
579 before the requiring thing. We break cycles at an arbitrary point,
580 and make no other guarantees.
581 """
582 # The current implementation, which we may change at any point
583 # installs the user specified things in the order given, except when
584 # dependencies must come earlier to achieve topological order.
585 order = []
586 ordered_reqs: Set[InstallRequirement] = set()
587
588 def schedule(req: InstallRequirement) -> None:
589 if req.satisfied_by or req in ordered_reqs:
590 return
592 return
594 for dep in self._discovered_dependencies[req.name]:
595 schedule(dep)
596 order.append(req)
597
598 for install_req in req_set.requirements.values():
599 schedule(install_req)
600 return order
bool _is_upgrade_allowed(self, InstallRequirement req)
Definition resolver.py:313
Tuple[List[InstallRequirement], Optional[InstallRequirement]] _add_requirement_to_set(self, RequirementSet requirement_set, InstallRequirement install_req, Optional[str] parent_req_name=None, Optional[Iterable[str]] extras_requested=None)
Definition resolver.py:201
List[InstallRequirement] get_installation_order(self, RequirementSet req_set)
Definition resolver.py:575
None _populate_link(self, InstallRequirement req)
Definition resolver.py:408
Optional[Link] _find_requirement_link(self, InstallRequirement req)
Definition resolver.py:386
Optional[str] _check_skip_installed(self, InstallRequirement req_to_install)
Definition resolver.py:334
None _set_req_to_reinstall(self, InstallRequirement req)
Definition resolver.py:322
RequirementSet resolve(self, List[InstallRequirement] root_reqs, bool check_supported_wheels)
Definition resolver.py:160
BaseDistribution _get_dist_for(self, InstallRequirement req)
Definition resolver.py:445
None __init__(self, RequirementPreparer preparer, PackageFinder finder, Optional[WheelCache] wheel_cache, InstallRequirementProvider make_install_req, bool use_user_site, bool ignore_dependencies, bool ignore_installed, bool ignore_requires_python, bool force_reinstall, str upgrade_strategy, Optional[Tuple[int,...]] py_version_info=None)
Definition resolver.py:133
List[InstallRequirement] _resolve_one(self, RequirementSet requirement_set, InstallRequirement req_to_install)
Definition resolver.py:495
None _check_dist_requires_python(BaseDistribution dist, Tuple[int, int, int] version_info, bool ignore_requires_python=False)
Definition resolver.py:62
for i