Mercurial > repos > guerler > hhblits
comparison lib/python3.8/site-packages/pip/_internal/legacy_resolve.py @ 1:64071f2a4cf0 draft default tip
Deleted selected files
| author | guerler |
|---|---|
| date | Mon, 27 Jul 2020 03:55:49 -0400 |
| parents | 9e54283cc701 |
| children |
comparison
equal
deleted
inserted
replaced
| 0:9e54283cc701 | 1:64071f2a4cf0 |
|---|---|
| 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 # mypy: disallow-untyped-defs=False | |
| 16 | |
| 17 import logging | |
| 18 import sys | |
| 19 from collections import defaultdict | |
| 20 from itertools import chain | |
| 21 | |
| 22 from pip._vendor.packaging import specifiers | |
| 23 | |
| 24 from pip._internal.exceptions import ( | |
| 25 BestVersionAlreadyInstalled, | |
| 26 DistributionNotFound, | |
| 27 HashError, | |
| 28 HashErrors, | |
| 29 UnsupportedPythonVersion, | |
| 30 ) | |
| 31 from pip._internal.utils.logging import indent_log | |
| 32 from pip._internal.utils.misc import dist_in_usersite, normalize_version_info | |
| 33 from pip._internal.utils.packaging import ( | |
| 34 check_requires_python, | |
| 35 get_requires_python, | |
| 36 ) | |
| 37 from pip._internal.utils.typing import MYPY_CHECK_RUNNING | |
| 38 | |
| 39 if MYPY_CHECK_RUNNING: | |
| 40 from typing import Callable, DefaultDict, List, Optional, Set, Tuple | |
| 41 from pip._vendor import pkg_resources | |
| 42 | |
| 43 from pip._internal.distributions import AbstractDistribution | |
| 44 from pip._internal.index.package_finder import PackageFinder | |
| 45 from pip._internal.operations.prepare import RequirementPreparer | |
| 46 from pip._internal.req.req_install import InstallRequirement | |
| 47 from pip._internal.req.req_set import RequirementSet | |
| 48 | |
| 49 InstallRequirementProvider = Callable[ | |
| 50 [str, InstallRequirement], InstallRequirement | |
| 51 ] | |
| 52 DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]] | |
| 53 | |
| 54 logger = logging.getLogger(__name__) | |
| 55 | |
| 56 | |
| 57 def _check_dist_requires_python( | |
| 58 dist, # type: pkg_resources.Distribution | |
| 59 version_info, # type: Tuple[int, int, int] | |
| 60 ignore_requires_python=False, # type: bool | |
| 61 ): | |
| 62 # type: (...) -> 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 requires_python = get_requires_python(dist) | |
| 76 try: | |
| 77 is_compatible = check_requires_python( | |
| 78 requires_python, version_info=version_info, | |
| 79 ) | |
| 80 except specifiers.InvalidSpecifier as exc: | |
| 81 logger.warning( | |
| 82 "Package %r has an invalid Requires-Python: %s", | |
| 83 dist.project_name, exc, | |
| 84 ) | |
| 85 return | |
| 86 | |
| 87 if is_compatible: | |
| 88 return | |
| 89 | |
| 90 version = '.'.join(map(str, version_info)) | |
| 91 if ignore_requires_python: | |
| 92 logger.debug( | |
| 93 'Ignoring failed Requires-Python check for package %r: ' | |
| 94 '%s not in %r', | |
| 95 dist.project_name, version, requires_python, | |
| 96 ) | |
| 97 return | |
| 98 | |
| 99 raise UnsupportedPythonVersion( | |
| 100 'Package {!r} requires a different Python: {} not in {!r}'.format( | |
| 101 dist.project_name, version, requires_python, | |
| 102 )) | |
| 103 | |
| 104 | |
| 105 class Resolver(object): | |
| 106 """Resolves which packages need to be installed/uninstalled to perform \ | |
| 107 the requested operation without breaking the requirements of any package. | |
| 108 """ | |
| 109 | |
| 110 _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} | |
| 111 | |
| 112 def __init__( | |
| 113 self, | |
| 114 preparer, # type: RequirementPreparer | |
| 115 finder, # type: PackageFinder | |
| 116 make_install_req, # type: InstallRequirementProvider | |
| 117 use_user_site, # type: bool | |
| 118 ignore_dependencies, # type: bool | |
| 119 ignore_installed, # type: bool | |
| 120 ignore_requires_python, # type: bool | |
| 121 force_reinstall, # type: bool | |
| 122 upgrade_strategy, # type: str | |
| 123 py_version_info=None, # type: Optional[Tuple[int, ...]] | |
| 124 ): | |
| 125 # type: (...) -> None | |
| 126 super(Resolver, self).__init__() | |
| 127 assert upgrade_strategy in self._allowed_strategies | |
| 128 | |
| 129 if py_version_info is None: | |
| 130 py_version_info = sys.version_info[:3] | |
| 131 else: | |
| 132 py_version_info = normalize_version_info(py_version_info) | |
| 133 | |
| 134 self._py_version_info = py_version_info | |
| 135 | |
| 136 self.preparer = preparer | |
| 137 self.finder = finder | |
| 138 | |
| 139 self.upgrade_strategy = upgrade_strategy | |
| 140 self.force_reinstall = force_reinstall | |
| 141 self.ignore_dependencies = ignore_dependencies | |
| 142 self.ignore_installed = ignore_installed | |
| 143 self.ignore_requires_python = ignore_requires_python | |
| 144 self.use_user_site = use_user_site | |
| 145 self._make_install_req = make_install_req | |
| 146 | |
| 147 self._discovered_dependencies = \ | |
| 148 defaultdict(list) # type: DiscoveredDependencies | |
| 149 | |
| 150 def resolve(self, requirement_set): | |
| 151 # type: (RequirementSet) -> None | |
| 152 """Resolve what operations need to be done | |
| 153 | |
| 154 As a side-effect of this method, the packages (and their dependencies) | |
| 155 are downloaded, unpacked and prepared for installation. This | |
| 156 preparation is done by ``pip.operations.prepare``. | |
| 157 | |
| 158 Once PyPI has static dependency metadata available, it would be | |
| 159 possible to move the preparation to become a step separated from | |
| 160 dependency resolution. | |
| 161 """ | |
| 162 # If any top-level requirement has a hash specified, enter | |
| 163 # hash-checking mode, which requires hashes from all. | |
| 164 root_reqs = ( | |
| 165 requirement_set.unnamed_requirements + | |
| 166 list(requirement_set.requirements.values()) | |
| 167 ) | |
| 168 | |
| 169 # Actually prepare the files, and collect any exceptions. Most hash | |
| 170 # exceptions cannot be checked ahead of time, because | |
| 171 # req.populate_link() needs to be called before we can make decisions | |
| 172 # based on link type. | |
| 173 discovered_reqs = [] # type: List[InstallRequirement] | |
| 174 hash_errors = HashErrors() | |
| 175 for req in chain(root_reqs, discovered_reqs): | |
| 176 try: | |
| 177 discovered_reqs.extend(self._resolve_one(requirement_set, req)) | |
| 178 except HashError as exc: | |
| 179 exc.req = req | |
| 180 hash_errors.append(exc) | |
| 181 | |
| 182 if hash_errors: | |
| 183 raise hash_errors | |
| 184 | |
| 185 def _is_upgrade_allowed(self, req): | |
| 186 # type: (InstallRequirement) -> bool | |
| 187 if self.upgrade_strategy == "to-satisfy-only": | |
| 188 return False | |
| 189 elif self.upgrade_strategy == "eager": | |
| 190 return True | |
| 191 else: | |
| 192 assert self.upgrade_strategy == "only-if-needed" | |
| 193 return req.is_direct | |
| 194 | |
| 195 def _set_req_to_reinstall(self, req): | |
| 196 # type: (InstallRequirement) -> None | |
| 197 """ | |
| 198 Set a requirement to be installed. | |
| 199 """ | |
| 200 # Don't uninstall the conflict if doing a user install and the | |
| 201 # conflict is not a user install. | |
| 202 if not self.use_user_site or dist_in_usersite(req.satisfied_by): | |
| 203 req.should_reinstall = True | |
| 204 req.satisfied_by = None | |
| 205 | |
| 206 def _check_skip_installed(self, req_to_install): | |
| 207 # type: (InstallRequirement) -> Optional[str] | |
| 208 """Check if req_to_install should be skipped. | |
| 209 | |
| 210 This will check if the req is installed, and whether we should upgrade | |
| 211 or reinstall it, taking into account all the relevant user options. | |
| 212 | |
| 213 After calling this req_to_install will only have satisfied_by set to | |
| 214 None if the req_to_install is to be upgraded/reinstalled etc. Any | |
| 215 other value will be a dist recording the current thing installed that | |
| 216 satisfies the requirement. | |
| 217 | |
| 218 Note that for vcs urls and the like we can't assess skipping in this | |
| 219 routine - we simply identify that we need to pull the thing down, | |
| 220 then later on it is pulled down and introspected to assess upgrade/ | |
| 221 reinstalls etc. | |
| 222 | |
| 223 :return: A text reason for why it was skipped, or None. | |
| 224 """ | |
| 225 if self.ignore_installed: | |
| 226 return None | |
| 227 | |
| 228 req_to_install.check_if_exists(self.use_user_site) | |
| 229 if not req_to_install.satisfied_by: | |
| 230 return None | |
| 231 | |
| 232 if self.force_reinstall: | |
| 233 self._set_req_to_reinstall(req_to_install) | |
| 234 return None | |
| 235 | |
| 236 if not self._is_upgrade_allowed(req_to_install): | |
| 237 if self.upgrade_strategy == "only-if-needed": | |
| 238 return 'already satisfied, skipping upgrade' | |
| 239 return 'already satisfied' | |
| 240 | |
| 241 # Check for the possibility of an upgrade. For link-based | |
| 242 # requirements we have to pull the tree down and inspect to assess | |
| 243 # the version #, so it's handled way down. | |
| 244 if not req_to_install.link: | |
| 245 try: | |
| 246 self.finder.find_requirement(req_to_install, upgrade=True) | |
| 247 except BestVersionAlreadyInstalled: | |
| 248 # Then the best version is installed. | |
| 249 return 'already up-to-date' | |
| 250 except DistributionNotFound: | |
| 251 # No distribution found, so we squash the error. It will | |
| 252 # be raised later when we re-try later to do the install. | |
| 253 # Why don't we just raise here? | |
| 254 pass | |
| 255 | |
| 256 self._set_req_to_reinstall(req_to_install) | |
| 257 return None | |
| 258 | |
| 259 def _get_abstract_dist_for(self, req): | |
| 260 # type: (InstallRequirement) -> AbstractDistribution | |
| 261 """Takes a InstallRequirement and returns a single AbstractDist \ | |
| 262 representing a prepared variant of the same. | |
| 263 """ | |
| 264 if req.editable: | |
| 265 return self.preparer.prepare_editable_requirement(req) | |
| 266 | |
| 267 # satisfied_by is only evaluated by calling _check_skip_installed, | |
| 268 # so it must be None here. | |
| 269 assert req.satisfied_by is None | |
| 270 skip_reason = self._check_skip_installed(req) | |
| 271 | |
| 272 if req.satisfied_by: | |
| 273 return self.preparer.prepare_installed_requirement( | |
| 274 req, skip_reason | |
| 275 ) | |
| 276 | |
| 277 upgrade_allowed = self._is_upgrade_allowed(req) | |
| 278 | |
| 279 # We eagerly populate the link, since that's our "legacy" behavior. | |
| 280 require_hashes = self.preparer.require_hashes | |
| 281 req.populate_link(self.finder, upgrade_allowed, require_hashes) | |
| 282 abstract_dist = self.preparer.prepare_linked_requirement(req) | |
| 283 | |
| 284 # NOTE | |
| 285 # The following portion is for determining if a certain package is | |
| 286 # going to be re-installed/upgraded or not and reporting to the user. | |
| 287 # This should probably get cleaned up in a future refactor. | |
| 288 | |
| 289 # req.req is only avail after unpack for URL | |
| 290 # pkgs repeat check_if_exists to uninstall-on-upgrade | |
| 291 # (#14) | |
| 292 if not self.ignore_installed: | |
| 293 req.check_if_exists(self.use_user_site) | |
| 294 | |
| 295 if req.satisfied_by: | |
| 296 should_modify = ( | |
| 297 self.upgrade_strategy != "to-satisfy-only" or | |
| 298 self.force_reinstall or | |
| 299 self.ignore_installed or | |
| 300 req.link.scheme == 'file' | |
| 301 ) | |
| 302 if should_modify: | |
| 303 self._set_req_to_reinstall(req) | |
| 304 else: | |
| 305 logger.info( | |
| 306 'Requirement already satisfied (use --upgrade to upgrade):' | |
| 307 ' %s', req, | |
| 308 ) | |
| 309 | |
| 310 return abstract_dist | |
| 311 | |
| 312 def _resolve_one( | |
| 313 self, | |
| 314 requirement_set, # type: RequirementSet | |
| 315 req_to_install, # type: InstallRequirement | |
| 316 ): | |
| 317 # type: (...) -> List[InstallRequirement] | |
| 318 """Prepare a single requirements file. | |
| 319 | |
| 320 :return: A list of additional InstallRequirements to also install. | |
| 321 """ | |
| 322 # Tell user what we are doing for this requirement: | |
| 323 # obtain (editable), skipping, processing (local url), collecting | |
| 324 # (remote url or package name) | |
| 325 if req_to_install.constraint or req_to_install.prepared: | |
| 326 return [] | |
| 327 | |
| 328 req_to_install.prepared = True | |
| 329 | |
| 330 # register tmp src for cleanup in case something goes wrong | |
| 331 requirement_set.reqs_to_cleanup.append(req_to_install) | |
| 332 | |
| 333 abstract_dist = self._get_abstract_dist_for(req_to_install) | |
| 334 | |
| 335 # Parse and return dependencies | |
| 336 dist = abstract_dist.get_pkg_resources_distribution() | |
| 337 # This will raise UnsupportedPythonVersion if the given Python | |
| 338 # version isn't compatible with the distribution's Requires-Python. | |
| 339 _check_dist_requires_python( | |
| 340 dist, version_info=self._py_version_info, | |
| 341 ignore_requires_python=self.ignore_requires_python, | |
| 342 ) | |
| 343 | |
| 344 more_reqs = [] # type: List[InstallRequirement] | |
| 345 | |
| 346 def add_req(subreq, extras_requested): | |
| 347 sub_install_req = self._make_install_req( | |
| 348 str(subreq), | |
| 349 req_to_install, | |
| 350 ) | |
| 351 parent_req_name = req_to_install.name | |
| 352 to_scan_again, add_to_parent = requirement_set.add_requirement( | |
| 353 sub_install_req, | |
| 354 parent_req_name=parent_req_name, | |
| 355 extras_requested=extras_requested, | |
| 356 ) | |
| 357 if parent_req_name and add_to_parent: | |
| 358 self._discovered_dependencies[parent_req_name].append( | |
| 359 add_to_parent | |
| 360 ) | |
| 361 more_reqs.extend(to_scan_again) | |
| 362 | |
| 363 with indent_log(): | |
| 364 # We add req_to_install before its dependencies, so that we | |
| 365 # can refer to it when adding dependencies. | |
| 366 if not requirement_set.has_requirement(req_to_install.name): | |
| 367 # 'unnamed' requirements will get added here | |
| 368 # 'unnamed' requirements can only come from being directly | |
| 369 # provided by the user. | |
| 370 assert req_to_install.is_direct | |
| 371 requirement_set.add_requirement( | |
| 372 req_to_install, parent_req_name=None, | |
| 373 ) | |
| 374 | |
| 375 if not self.ignore_dependencies: | |
| 376 if req_to_install.extras: | |
| 377 logger.debug( | |
| 378 "Installing extra requirements: %r", | |
| 379 ','.join(req_to_install.extras), | |
| 380 ) | |
| 381 missing_requested = sorted( | |
| 382 set(req_to_install.extras) - set(dist.extras) | |
| 383 ) | |
| 384 for missing in missing_requested: | |
| 385 logger.warning( | |
| 386 '%s does not provide the extra \'%s\'', | |
| 387 dist, missing | |
| 388 ) | |
| 389 | |
| 390 available_requested = sorted( | |
| 391 set(dist.extras) & set(req_to_install.extras) | |
| 392 ) | |
| 393 for subreq in dist.requires(available_requested): | |
| 394 add_req(subreq, extras_requested=available_requested) | |
| 395 | |
| 396 if not req_to_install.editable and not req_to_install.satisfied_by: | |
| 397 # XXX: --no-install leads this to report 'Successfully | |
| 398 # downloaded' for only non-editable reqs, even though we took | |
| 399 # action on them. | |
| 400 requirement_set.successfully_downloaded.append(req_to_install) | |
| 401 | |
| 402 return more_reqs | |
| 403 | |
| 404 def get_installation_order(self, req_set): | |
| 405 # type: (RequirementSet) -> List[InstallRequirement] | |
| 406 """Create the installation order. | |
| 407 | |
| 408 The installation order is topological - requirements are installed | |
| 409 before the requiring thing. We break cycles at an arbitrary point, | |
| 410 and make no other guarantees. | |
| 411 """ | |
| 412 # The current implementation, which we may change at any point | |
| 413 # installs the user specified things in the order given, except when | |
| 414 # dependencies must come earlier to achieve topological order. | |
| 415 order = [] | |
| 416 ordered_reqs = set() # type: Set[InstallRequirement] | |
| 417 | |
| 418 def schedule(req): | |
| 419 if req.satisfied_by or req in ordered_reqs: | |
| 420 return | |
| 421 if req.constraint: | |
| 422 return | |
| 423 ordered_reqs.add(req) | |
| 424 for dep in self._discovered_dependencies[req.name]: | |
| 425 schedule(dep) | |
| 426 order.append(req) | |
| 427 | |
| 428 for install_req in req_set.requirements.values(): | |
| 429 schedule(install_req) | |
| 430 return order |
