test
This commit is contained in:
		| @ -0,0 +1,600 @@ | ||||
| """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) | ||||
| """ | ||||
|  | ||||
| # The following comment should be removed at some point in the future. | ||||
| # mypy: strict-optional=False | ||||
|  | ||||
| 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[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. | ||||
|     """ | ||||
|     # This idiosyncratically converts the SpecifierSet to str and let | ||||
|     # check_requires_python then parse it again into SpecifierSet. But this | ||||
|     # is the legacy resolver so I'm just not going to bother refactoring. | ||||
|     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( | ||||
|         "Package {!r} requires a different Python: {} not in {!r}".format( | ||||
|             dist.raw_name, version, requires_python | ||||
|         ) | ||||
|     ) | ||||
|  | ||||
|  | ||||
| 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) | ||||
|  | ||||
|         # Actually prepare the files, and collect any exceptions. Most hash | ||||
|         # exceptions cannot be checked ahead of time, because | ||||
|         # _populate_link() needs to be called before we can make decisions | ||||
|         # based on link type. | ||||
|         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 the markers do not match, ignore this requirement. | ||||
|         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 the wheel is not supported, raise an error. | ||||
|         # Should check this after filtering out based on environment markers to | ||||
|         # allow specifying different wheels based on the environment/OS, in a | ||||
|         # single requirements file. | ||||
|         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( | ||||
|                     "{} is not a supported wheel on this platform.".format( | ||||
|                         wheel.filename | ||||
|                     ) | ||||
|                 ) | ||||
|  | ||||
|         # This next bit is really a sanity check. | ||||
|         assert ( | ||||
|             not install_req.user_supplied or parent_req_name is None | ||||
|         ), "a user supplied req shouldn't have a parent" | ||||
|  | ||||
|         # Unnamed requirements are scanned again and the requirement won't be | ||||
|         # added as a dependency until after scanning. | ||||
|         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( | ||||
|                 "Double requirement given: {} (already in {}, name={!r})".format( | ||||
|                     install_req, existing_req, install_req.name | ||||
|                 ) | ||||
|             ) | ||||
|  | ||||
|         # When no existing requirement exists, add the requirement as a | ||||
|         # dependency and it will be scanned again after. | ||||
|         if not existing_req: | ||||
|             requirement_set.add_named_requirement(install_req) | ||||
|             # We'd want to rescan this requirement later | ||||
|             return [install_req], install_req | ||||
|  | ||||
|         # Assume there's no need to scan, and that we've already | ||||
|         # encountered this for scanning. | ||||
|         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( | ||||
|                 "Could not satisfy constraints for '{}': " | ||||
|                 "installation from path or url cannot be " | ||||
|                 "constrained to a version".format(install_req.name) | ||||
|             ) | ||||
|         # If we're now installing a constraint, mark the existing | ||||
|         # object for real installation. | ||||
|         existing_req.constraint = False | ||||
|         # If we're now installing a user supplied requirement, | ||||
|         # mark the existing object as such. | ||||
|         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 the existing requirement for addition to the parent and | ||||
|         # scanning again. | ||||
|         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. | ||||
|         """ | ||||
|         # Don't uninstall the conflict if doing a user install and the | ||||
|         # conflict is not a user install. | ||||
|         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" | ||||
|  | ||||
|         # Check for the possibility of an upgrade.  For link-based | ||||
|         # requirements we have to pull the tree down and inspect to assess | ||||
|         # the version #, so it's handled way down. | ||||
|         if not req_to_install.link: | ||||
|             try: | ||||
|                 self.finder.find_requirement(req_to_install, upgrade=True) | ||||
|             except BestVersionAlreadyInstalled: | ||||
|                 # Then the best version is installed. | ||||
|                 return "already up-to-date" | ||||
|             except DistributionNotFound: | ||||
|                 # No distribution found, so we squash the error.  It will | ||||
|                 # be raised later when we re-try later to do the install. | ||||
|                 # Why don't we just raise here? | ||||
|                 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 | ||||
|  | ||||
|         # Log a warning per PEP 592 if necessary before returning. | ||||
|         link = best_candidate.link | ||||
|         if link.is_yanked: | ||||
|             reason = link.yanked_reason or "<none given>" | ||||
|             msg = ( | ||||
|                 # Mark this as a unicode string to prevent | ||||
|                 # "UnicodeEncodeError: 'ascii' codec can't encode character" | ||||
|                 # in Python 2 when the reason contains non-ascii characters. | ||||
|                 "The candidate selected for download or install is a " | ||||
|                 "yanked version: {candidate}\n" | ||||
|                 "Reason for being yanked: {reason}" | ||||
|             ).format(candidate=best_candidate, reason=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 | ||||
|         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.original_link_is_in_wheel_cache = True | ||||
|             if cache_entry.origin is not None: | ||||
|                 req.download_info = cache_entry.origin | ||||
|             else: | ||||
|                 # Legacy cache entry that does not have origin.json. | ||||
|                 # download_info may miss the archive_info.hash field. | ||||
|                 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) | ||||
|  | ||||
|         # satisfied_by is only evaluated by calling _check_skip_installed, | ||||
|         # so it must be None here. | ||||
|         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) | ||||
|  | ||||
|         # We eagerly populate the link, since that's our "legacy" behavior. | ||||
|         self._populate_link(req) | ||||
|         dist = self.preparer.prepare_linked_requirement(req) | ||||
|  | ||||
|         # NOTE | ||||
|         # The following portion is for determining if a certain package is | ||||
|         # going to be re-installed/upgraded or not and reporting to the user. | ||||
|         # This should probably get cleaned up in a future refactor. | ||||
|  | ||||
|         # req.req is only avail after unpack for URL | ||||
|         # pkgs repeat check_if_exists to uninstall-on-upgrade | ||||
|         # (#14) | ||||
|         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. | ||||
|         """ | ||||
|         # Tell user what we are doing for this requirement: | ||||
|         # obtain (editable), skipping, processing (local url), collecting | ||||
|         # (remote url or package name) | ||||
|         if req_to_install.constraint or req_to_install.prepared: | ||||
|             return [] | ||||
|  | ||||
|         req_to_install.prepared = True | ||||
|  | ||||
|         # Parse and return dependencies | ||||
|         dist = self._get_dist_for(req_to_install) | ||||
|         # This will raise UnsupportedPythonVersion if the given Python | ||||
|         # version isn't compatible with the distribution's Requires-Python. | ||||
|         _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: | ||||
|             # This idiosyncratically converts the Requirement to str and let | ||||
|             # make_install_req then parse it again into Requirement. But this is | ||||
|             # the legacy resolver so I'm just not going to bother refactoring. | ||||
|             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(): | ||||
|             # We add req_to_install before its dependencies, so that we | ||||
|             # can refer to it when adding dependencies. | ||||
|             if not requirement_set.has_requirement(req_to_install.name): | ||||
|                 # 'unnamed' requirements will get added here | ||||
|                 # 'unnamed' requirements can only come from being directly | ||||
|                 # provided by the user. | ||||
|                 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. | ||||
|         """ | ||||
|         # The current implementation, which we may change at any point | ||||
|         # installs the user specified things in the order given, except when | ||||
|         # dependencies must come earlier to achieve topological order. | ||||
|         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 | ||||
		Reference in New Issue
	
	Block a user