python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_internal/
resolution/
legacy/
resolver.py
       1  """Dependency Resolution
       2  
       3  The dependency resolution in pip is performed as follows:
       4  
       5  for 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.
       9  for 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  
      16  import logging
      17  import sys
      18  from collections import defaultdict
      19  from itertools import chain
      20  from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
      21  
      22  from pip._vendor.packaging import specifiers
      23  from pip._vendor.packaging.requirements import Requirement
      24  
      25  from pip._internal.cache import WheelCache
      26  from pip._internal.exceptions import (
      27      BestVersionAlreadyInstalled,
      28      DistributionNotFound,
      29      HashError,
      30      HashErrors,
      31      InstallationError,
      32      NoneMetadataError,
      33      UnsupportedPythonVersion,
      34  )
      35  from pip._internal.index.package_finder import PackageFinder
      36  from pip._internal.metadata import BaseDistribution
      37  from pip._internal.models.link import Link
      38  from pip._internal.models.wheel import Wheel
      39  from pip._internal.operations.prepare import RequirementPreparer
      40  from pip._internal.req.req_install import (
      41      InstallRequirement,
      42      check_invalid_constraint_type,
      43  )
      44  from pip._internal.req.req_set import RequirementSet
      45  from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
      46  from pip._internal.utils import compatibility_tags
      47  from pip._internal.utils.compatibility_tags import get_supported
      48  from pip._internal.utils.direct_url_helpers import direct_url_from_link
      49  from pip._internal.utils.logging import indent_log
      50  from pip._internal.utils.misc import normalize_version_info
      51  from pip._internal.utils.packaging import check_requires_python
      52  
      53  logger = logging.getLogger(__name__)
      54  
      55  DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
      56  
      57  
      58  def _check_dist_requires_python(
      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:
      88          logger.warning(
      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:
      98          logger.debug(
      99              "Ignoring failed Requires-Python check for package %r: %s not in %r",
     100              dist.raw_name,
     101              version,
     102              requires_python,
     103          )
     104          return
     105  
     106      raise UnsupportedPythonVersion(
     107          "Package {!r} requires a different Python: {} not in {!r}".format(
     108              dist.raw_name, version, requires_python
     109          )
     110      )
     111  
     112  
     113  class ESC[4;38;5;81mResolver(ESC[4;38;5;149mBaseResolver):
     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  
     120      def __init__(
     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  
     158      def resolve(
     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:
     173              if req.constraint:
     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
     188                  hash_errors.append(exc)
     189  
     190          if hash_errors:
     191              raise hash_errors
     192  
     193          return requirement_set
     194  
     195      def _add_requirement_to_set(
     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):
     218              logger.info(
     219                  "Ignoring %s: markers '%s' don't match your environment",
     220                  install_req.name,
     221                  install_req.markers,
     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.
     229          if install_req.link and install_req.link.is_wheel:
     230              wheel = Wheel(install_req.link.filename)
     231              tags = compatibility_tags.get_supported()
     232              if requirement_set.check_supported_wheels and not wheel.supported(tags):
     233                  raise InstallationError(
     234                      "{} is not a supported wheel on this platform.".format(
     235                          wheel.filename
     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:
     247              requirement_set.add_unnamed_requirement(install_req)
     248              return [install_req], None
     249  
     250          try:
     251              existing_req: Optional[
     252                  InstallRequirement
     253              ] = requirement_set.get_requirement(install_req.name)
     254          except KeyError:
     255              existing_req = None
     256  
     257          has_conflicting_requirement = (
     258              parent_req_name is None
     259              and existing_req
     260              and not existing_req.constraint
     261              and existing_req.extras == install_req.extras
     262              and existing_req.req
     263              and install_req.req
     264              and existing_req.req.specifier != install_req.req.specifier
     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:
     276              requirement_set.add_named_requirement(install_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.
     282          if install_req.constraint or not existing_req.constraint:
     283              return [], existing_req
     284  
     285          does_not_satisfy_constraint = install_req.link and not (
     286              existing_req.link and install_req.link.path == existing_req.link.path
     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.
     296          existing_req.constraint = False
     297          # If we're now installing a user supplied requirement,
     298          # mark the existing object as such.
     299          if install_req.user_supplied:
     300              existing_req.user_supplied = True
     301          existing_req.extras = tuple(
     302              sorted(set(existing_req.extras) | set(install_req.extras))
     303          )
     304          logger.debug(
     305              "Setting %s extras to: %s",
     306              existing_req,
     307              existing_req.extras,
     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"
     320              return req.user_supplied or req.constraint
     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.
     328          if not self.use_user_site or req.satisfied_by.in_usersite:
     329              req.should_reinstall = True
     330          req.satisfied_by = None
     331  
     332      def _check_skip_installed(
     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  
     355          req_to_install.check_if_exists(self.use_user_site)
     356          if not req_to_install.satisfied_by:
     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.
     393          link = best_candidate.link
     394          if link.is_yanked:
     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:
     422              req.link = self._find_requirement_link(req)
     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)
     433              if req.link is req.original_link and cache_entry.persistent:
     434                  req.cached_wheel_source_link = req.link
     435              if cache_entry.origin is not None:
     436                  req.download_info = cache_entry.origin
     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                  )
     443              req.link = cache_entry.link
     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  
     457          if req.satisfied_by:
     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:
     473              req.check_if_exists(self.use_user_site)
     474  
     475          if req.satisfied_by:
     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:
     485                  logger.info(
     486                      "Requirement already satisfied (use --upgrade to upgrade): %s",
     487                      req,
     488                  )
     489          return dist
     490  
     491      def _resolve_one(
     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)
     503          if req_to_install.constraint or req_to_install.prepared:
     504              return []
     505  
     506          req_to_install.prepared = True
     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.
     512          _check_dist_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.
     539              if not requirement_set.has_requirement(req_to_install.name):
     540                  # 'unnamed' requirements will get added here
     541                  # 'unnamed' requirements can only come from being directly
     542                  # provided by the user.
     543                  assert req_to_install.user_supplied
     544                  self._add_requirement_to_set(
     545                      requirement_set, req_to_install, parent_req_name=None
     546                  )
     547  
     548              if not self.ignore_dependencies:
     549                  if req_to_install.extras:
     550                      logger.debug(
     551                          "Installing extra requirements: %r",
     552                          ",".join(req_to_install.extras),
     553                      )
     554                  missing_requested = sorted(
     555                      set(req_to_install.extras) - set(dist.iter_provided_extras())
     556                  )
     557                  for missing in missing_requested:
     558                      logger.warning(
     559                          "%s %s does not provide the extra '%s'",
     560                          dist.raw_name,
     561                          dist.version,
     562                          missing,
     563                      )
     564  
     565                  available_requested = sorted(
     566                      set(dist.iter_provided_extras()) & set(req_to_install.extras)
     567                  )
     568                  for subreq in dist.iter_dependencies(available_requested):
     569                      add_req(subreq, extras_requested=available_requested)
     570  
     571          return more_reqs
     572  
     573      def get_installation_order(
     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
     591              if req.constraint:
     592                  return
     593              ordered_reqs.add(req)
     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