| """Dependency Resolution |
| |
| The dependency resolution in pip is performed as follows: |
| |
| for top-level requirements: |
| a. only one spec allowed per project, regardless of conflicts or not. |
| otherwise a "double requirement" exception is raised |
| b. they override sub-dependency requirements. |
| for sub-dependencies |
| a. "first found, wins" (where the order is breadth first) |
| """ |
|
|
| import logging |
| import sys |
| from collections import defaultdict |
| from itertools import chain |
| from typing import DefaultDict, Iterable, List, Optional, Set, Tuple |
|
|
| from pip._vendor.packaging import specifiers |
| from pip._vendor.packaging.requirements import Requirement |
|
|
| from pip._internal.cache import WheelCache |
| from pip._internal.exceptions import ( |
| BestVersionAlreadyInstalled, |
| DistributionNotFound, |
| HashError, |
| HashErrors, |
| InstallationError, |
| NoneMetadataError, |
| UnsupportedPythonVersion, |
| ) |
| from pip._internal.index.package_finder import PackageFinder |
| from pip._internal.metadata import BaseDistribution |
| from pip._internal.models.link import Link |
| from pip._internal.models.wheel import Wheel |
| from pip._internal.operations.prepare import RequirementPreparer |
| from pip._internal.req.req_install import ( |
| InstallRequirement, |
| check_invalid_constraint_type, |
| ) |
| from pip._internal.req.req_set import RequirementSet |
| from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider |
| from pip._internal.utils import compatibility_tags |
| from pip._internal.utils.compatibility_tags import get_supported |
| from pip._internal.utils.direct_url_helpers import direct_url_from_link |
| from pip._internal.utils.logging import indent_log |
| from pip._internal.utils.misc import normalize_version_info |
| from pip._internal.utils.packaging import check_requires_python |
|
|
| logger = logging.getLogger(__name__) |
|
|
| DiscoveredDependencies = DefaultDict[Optional[str], List[InstallRequirement]] |
|
|
|
|
| def _check_dist_requires_python( |
| dist: BaseDistribution, |
| version_info: Tuple[int, int, int], |
| ignore_requires_python: bool = False, |
| ) -> None: |
| """ |
| Check whether the given Python version is compatible with a distribution's |
| "Requires-Python" value. |
| |
| :param version_info: A 3-tuple of ints representing the Python |
| major-minor-micro version to check. |
| :param ignore_requires_python: Whether to ignore the "Requires-Python" |
| value if the given Python version isn't compatible. |
| |
| :raises UnsupportedPythonVersion: When the given Python version isn't |
| compatible. |
| """ |
| |
| |
| |
| try: |
| requires_python = str(dist.requires_python) |
| except FileNotFoundError as e: |
| raise NoneMetadataError(dist, str(e)) |
| try: |
| is_compatible = check_requires_python( |
| requires_python, |
| version_info=version_info, |
| ) |
| except specifiers.InvalidSpecifier as exc: |
| logger.warning( |
| "Package %r has an invalid Requires-Python: %s", dist.raw_name, exc |
| ) |
| return |
|
|
| if is_compatible: |
| return |
|
|
| version = ".".join(map(str, version_info)) |
| if ignore_requires_python: |
| logger.debug( |
| "Ignoring failed Requires-Python check for package %r: %s not in %r", |
| dist.raw_name, |
| version, |
| requires_python, |
| ) |
| return |
|
|
| raise UnsupportedPythonVersion( |
| f"Package {dist.raw_name!r} requires a different Python: " |
| f"{version} not in {requires_python!r}" |
| ) |
|
|
|
|
| class Resolver(BaseResolver): |
| """Resolves which packages need to be installed/uninstalled to perform \ |
| the requested operation without breaking the requirements of any package. |
| """ |
|
|
| _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} |
|
|
| def __init__( |
| self, |
| preparer: RequirementPreparer, |
| finder: PackageFinder, |
| wheel_cache: Optional[WheelCache], |
| make_install_req: InstallRequirementProvider, |
| use_user_site: bool, |
| ignore_dependencies: bool, |
| ignore_installed: bool, |
| ignore_requires_python: bool, |
| force_reinstall: bool, |
| upgrade_strategy: str, |
| py_version_info: Optional[Tuple[int, ...]] = None, |
| ) -> None: |
| super().__init__() |
| assert upgrade_strategy in self._allowed_strategies |
|
|
| if py_version_info is None: |
| py_version_info = sys.version_info[:3] |
| else: |
| py_version_info = normalize_version_info(py_version_info) |
|
|
| self._py_version_info = py_version_info |
|
|
| self.preparer = preparer |
| self.finder = finder |
| self.wheel_cache = wheel_cache |
|
|
| self.upgrade_strategy = upgrade_strategy |
| self.force_reinstall = force_reinstall |
| self.ignore_dependencies = ignore_dependencies |
| self.ignore_installed = ignore_installed |
| self.ignore_requires_python = ignore_requires_python |
| self.use_user_site = use_user_site |
| self._make_install_req = make_install_req |
|
|
| self._discovered_dependencies: DiscoveredDependencies = defaultdict(list) |
|
|
| def resolve( |
| self, root_reqs: List[InstallRequirement], check_supported_wheels: bool |
| ) -> RequirementSet: |
| """Resolve what operations need to be done |
| |
| As a side-effect of this method, the packages (and their dependencies) |
| are downloaded, unpacked and prepared for installation. This |
| preparation is done by ``pip.operations.prepare``. |
| |
| Once PyPI has static dependency metadata available, it would be |
| possible to move the preparation to become a step separated from |
| dependency resolution. |
| """ |
| requirement_set = RequirementSet(check_supported_wheels=check_supported_wheels) |
| for req in root_reqs: |
| if req.constraint: |
| check_invalid_constraint_type(req) |
| self._add_requirement_to_set(requirement_set, req) |
|
|
| |
| |
| |
| |
| discovered_reqs: List[InstallRequirement] = [] |
| hash_errors = HashErrors() |
| for req in chain(requirement_set.all_requirements, discovered_reqs): |
| try: |
| discovered_reqs.extend(self._resolve_one(requirement_set, req)) |
| except HashError as exc: |
| exc.req = req |
| hash_errors.append(exc) |
|
|
| if hash_errors: |
| raise hash_errors |
|
|
| return requirement_set |
|
|
| def _add_requirement_to_set( |
| self, |
| requirement_set: RequirementSet, |
| install_req: InstallRequirement, |
| parent_req_name: Optional[str] = None, |
| extras_requested: Optional[Iterable[str]] = None, |
| ) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]: |
| """Add install_req as a requirement to install. |
| |
| :param parent_req_name: The name of the requirement that needed this |
| added. The name is used because when multiple unnamed requirements |
| resolve to the same name, we could otherwise end up with dependency |
| links that point outside the Requirements set. parent_req must |
| already be added. Note that None implies that this is a user |
| supplied requirement, vs an inferred one. |
| :param extras_requested: an iterable of extras used to evaluate the |
| environment markers. |
| :return: Additional requirements to scan. That is either [] if |
| the requirement is not applicable, or [install_req] if the |
| requirement is applicable and has just been added. |
| """ |
| |
| if not install_req.match_markers(extras_requested): |
| logger.info( |
| "Ignoring %s: markers '%s' don't match your environment", |
| install_req.name, |
| install_req.markers, |
| ) |
| return [], None |
|
|
| |
| |
| |
| |
| if install_req.link and install_req.link.is_wheel: |
| wheel = Wheel(install_req.link.filename) |
| tags = compatibility_tags.get_supported() |
| if requirement_set.check_supported_wheels and not wheel.supported(tags): |
| raise InstallationError( |
| f"{wheel.filename} is not a supported wheel on this platform." |
| ) |
|
|
| |
| assert ( |
| not install_req.user_supplied or parent_req_name is None |
| ), "a user supplied req shouldn't have a parent" |
|
|
| |
| |
| if not install_req.name: |
| requirement_set.add_unnamed_requirement(install_req) |
| return [install_req], None |
|
|
| try: |
| existing_req: Optional[InstallRequirement] = ( |
| requirement_set.get_requirement(install_req.name) |
| ) |
| except KeyError: |
| existing_req = None |
|
|
| has_conflicting_requirement = ( |
| parent_req_name is None |
| and existing_req |
| and not existing_req.constraint |
| and existing_req.extras == install_req.extras |
| and existing_req.req |
| and install_req.req |
| and existing_req.req.specifier != install_req.req.specifier |
| ) |
| if has_conflicting_requirement: |
| raise InstallationError( |
| f"Double requirement given: {install_req} " |
| f"(already in {existing_req}, name={install_req.name!r})" |
| ) |
|
|
| |
| |
| if not existing_req: |
| requirement_set.add_named_requirement(install_req) |
| |
| return [install_req], install_req |
|
|
| |
| |
| if install_req.constraint or not existing_req.constraint: |
| return [], existing_req |
|
|
| does_not_satisfy_constraint = install_req.link and not ( |
| existing_req.link and install_req.link.path == existing_req.link.path |
| ) |
| if does_not_satisfy_constraint: |
| raise InstallationError( |
| f"Could not satisfy constraints for '{install_req.name}': " |
| "installation from path or url cannot be " |
| "constrained to a version" |
| ) |
| |
| |
| existing_req.constraint = False |
| |
| |
| if install_req.user_supplied: |
| existing_req.user_supplied = True |
| existing_req.extras = tuple( |
| sorted(set(existing_req.extras) | set(install_req.extras)) |
| ) |
| logger.debug( |
| "Setting %s extras to: %s", |
| existing_req, |
| existing_req.extras, |
| ) |
| |
| |
| return [existing_req], existing_req |
|
|
| def _is_upgrade_allowed(self, req: InstallRequirement) -> bool: |
| if self.upgrade_strategy == "to-satisfy-only": |
| return False |
| elif self.upgrade_strategy == "eager": |
| return True |
| else: |
| assert self.upgrade_strategy == "only-if-needed" |
| return req.user_supplied or req.constraint |
|
|
| def _set_req_to_reinstall(self, req: InstallRequirement) -> None: |
| """ |
| Set a requirement to be installed. |
| """ |
| |
| |
| assert req.satisfied_by is not None |
| if not self.use_user_site or req.satisfied_by.in_usersite: |
| req.should_reinstall = True |
| req.satisfied_by = None |
|
|
| def _check_skip_installed( |
| self, req_to_install: InstallRequirement |
| ) -> Optional[str]: |
| """Check if req_to_install should be skipped. |
| |
| This will check if the req is installed, and whether we should upgrade |
| or reinstall it, taking into account all the relevant user options. |
| |
| After calling this req_to_install will only have satisfied_by set to |
| None if the req_to_install is to be upgraded/reinstalled etc. Any |
| other value will be a dist recording the current thing installed that |
| satisfies the requirement. |
| |
| Note that for vcs urls and the like we can't assess skipping in this |
| routine - we simply identify that we need to pull the thing down, |
| then later on it is pulled down and introspected to assess upgrade/ |
| reinstalls etc. |
| |
| :return: A text reason for why it was skipped, or None. |
| """ |
| if self.ignore_installed: |
| return None |
|
|
| req_to_install.check_if_exists(self.use_user_site) |
| if not req_to_install.satisfied_by: |
| return None |
|
|
| if self.force_reinstall: |
| self._set_req_to_reinstall(req_to_install) |
| return None |
|
|
| if not self._is_upgrade_allowed(req_to_install): |
| if self.upgrade_strategy == "only-if-needed": |
| return "already satisfied, skipping upgrade" |
| return "already satisfied" |
|
|
| |
| |
| |
| if not req_to_install.link: |
| try: |
| self.finder.find_requirement(req_to_install, upgrade=True) |
| except BestVersionAlreadyInstalled: |
| |
| return "already up-to-date" |
| except DistributionNotFound: |
| |
| |
| |
| pass |
|
|
| self._set_req_to_reinstall(req_to_install) |
| return None |
|
|
| def _find_requirement_link(self, req: InstallRequirement) -> Optional[Link]: |
| upgrade = self._is_upgrade_allowed(req) |
| best_candidate = self.finder.find_requirement(req, upgrade) |
| if not best_candidate: |
| return None |
|
|
| |
| link = best_candidate.link |
| if link.is_yanked: |
| reason = link.yanked_reason or "<none given>" |
| msg = ( |
| |
| |
| |
| "The candidate selected for download or install is a " |
| f"yanked version: {best_candidate}\n" |
| f"Reason for being yanked: {reason}" |
| ) |
| logger.warning(msg) |
|
|
| return link |
|
|
| def _populate_link(self, req: InstallRequirement) -> None: |
| """Ensure that if a link can be found for this, that it is found. |
| |
| Note that req.link may still be None - if the requirement is already |
| installed and not needed to be upgraded based on the return value of |
| _is_upgrade_allowed(). |
| |
| If preparer.require_hashes is True, don't use the wheel cache, because |
| cached wheels, always built locally, have different hashes than the |
| files downloaded from the index server and thus throw false hash |
| mismatches. Furthermore, cached wheels at present have undeterministic |
| contents due to file modification times. |
| """ |
| if req.link is None: |
| req.link = self._find_requirement_link(req) |
|
|
| if self.wheel_cache is None or self.preparer.require_hashes: |
| return |
|
|
| assert req.link is not None, "_find_requirement_link unexpectedly returned None" |
| cache_entry = self.wheel_cache.get_cache_entry( |
| link=req.link, |
| package_name=req.name, |
| supported_tags=get_supported(), |
| ) |
| if cache_entry is not None: |
| logger.debug("Using cached wheel link: %s", cache_entry.link) |
| if req.link is req.original_link and cache_entry.persistent: |
| req.cached_wheel_source_link = req.link |
| if cache_entry.origin is not None: |
| req.download_info = cache_entry.origin |
| else: |
| |
| |
| req.download_info = direct_url_from_link( |
| req.link, link_is_in_wheel_cache=cache_entry.persistent |
| ) |
| req.link = cache_entry.link |
|
|
| def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution: |
| """Takes a InstallRequirement and returns a single AbstractDist \ |
| representing a prepared variant of the same. |
| """ |
| if req.editable: |
| return self.preparer.prepare_editable_requirement(req) |
|
|
| |
| |
| assert req.satisfied_by is None |
| skip_reason = self._check_skip_installed(req) |
|
|
| if req.satisfied_by: |
| return self.preparer.prepare_installed_requirement(req, skip_reason) |
|
|
| |
| self._populate_link(req) |
| dist = self.preparer.prepare_linked_requirement(req) |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| if not self.ignore_installed: |
| req.check_if_exists(self.use_user_site) |
|
|
| if req.satisfied_by: |
| should_modify = ( |
| self.upgrade_strategy != "to-satisfy-only" |
| or self.force_reinstall |
| or self.ignore_installed |
| or req.link.scheme == "file" |
| ) |
| if should_modify: |
| self._set_req_to_reinstall(req) |
| else: |
| logger.info( |
| "Requirement already satisfied (use --upgrade to upgrade): %s", |
| req, |
| ) |
| return dist |
|
|
| def _resolve_one( |
| self, |
| requirement_set: RequirementSet, |
| req_to_install: InstallRequirement, |
| ) -> List[InstallRequirement]: |
| """Prepare a single requirements file. |
| |
| :return: A list of additional InstallRequirements to also install. |
| """ |
| |
| |
| |
| if req_to_install.constraint or req_to_install.prepared: |
| return [] |
|
|
| req_to_install.prepared = True |
|
|
| |
| dist = self._get_dist_for(req_to_install) |
| |
| |
| _check_dist_requires_python( |
| dist, |
| version_info=self._py_version_info, |
| ignore_requires_python=self.ignore_requires_python, |
| ) |
|
|
| more_reqs: List[InstallRequirement] = [] |
|
|
| def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None: |
| |
| |
| |
| sub_install_req = self._make_install_req(str(subreq), req_to_install) |
| parent_req_name = req_to_install.name |
| to_scan_again, add_to_parent = self._add_requirement_to_set( |
| requirement_set, |
| sub_install_req, |
| parent_req_name=parent_req_name, |
| extras_requested=extras_requested, |
| ) |
| if parent_req_name and add_to_parent: |
| self._discovered_dependencies[parent_req_name].append(add_to_parent) |
| more_reqs.extend(to_scan_again) |
|
|
| with indent_log(): |
| |
| |
| assert req_to_install.name is not None |
| if not requirement_set.has_requirement(req_to_install.name): |
| |
| |
| |
| assert req_to_install.user_supplied |
| self._add_requirement_to_set( |
| requirement_set, req_to_install, parent_req_name=None |
| ) |
|
|
| if not self.ignore_dependencies: |
| if req_to_install.extras: |
| logger.debug( |
| "Installing extra requirements: %r", |
| ",".join(req_to_install.extras), |
| ) |
| missing_requested = sorted( |
| set(req_to_install.extras) - set(dist.iter_provided_extras()) |
| ) |
| for missing in missing_requested: |
| logger.warning( |
| "%s %s does not provide the extra '%s'", |
| dist.raw_name, |
| dist.version, |
| missing, |
| ) |
|
|
| available_requested = sorted( |
| set(dist.iter_provided_extras()) & set(req_to_install.extras) |
| ) |
| for subreq in dist.iter_dependencies(available_requested): |
| add_req(subreq, extras_requested=available_requested) |
|
|
| return more_reqs |
|
|
| def get_installation_order( |
| self, req_set: RequirementSet |
| ) -> List[InstallRequirement]: |
| """Create the installation order. |
| |
| The installation order is topological - requirements are installed |
| before the requiring thing. We break cycles at an arbitrary point, |
| and make no other guarantees. |
| """ |
| |
| |
| |
| order = [] |
| ordered_reqs: Set[InstallRequirement] = set() |
|
|
| def schedule(req: InstallRequirement) -> None: |
| if req.satisfied_by or req in ordered_reqs: |
| return |
| if req.constraint: |
| return |
| ordered_reqs.add(req) |
| for dep in self._discovered_dependencies[req.name]: |
| schedule(dep) |
| order.append(req) |
|
|
| for install_req in req_set.requirements.values(): |
| schedule(install_req) |
| return order |
|
|