Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/operations/check.py
4804 views
"""Validation of dependencies of packages1"""23import logging4from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple56from pip._vendor.packaging.requirements import Requirement7from pip._vendor.packaging.utils import NormalizedName, canonicalize_name89from pip._internal.distributions import make_distribution_for_install_requirement10from pip._internal.metadata import get_default_environment11from pip._internal.metadata.base import DistributionVersion12from pip._internal.req.req_install import InstallRequirement1314logger = logging.getLogger(__name__)151617class PackageDetails(NamedTuple):18version: DistributionVersion19dependencies: List[Requirement]202122# Shorthands23PackageSet = Dict[NormalizedName, PackageDetails]24Missing = Tuple[NormalizedName, Requirement]25Conflicting = Tuple[NormalizedName, DistributionVersion, Requirement]2627MissingDict = Dict[NormalizedName, List[Missing]]28ConflictingDict = Dict[NormalizedName, List[Conflicting]]29CheckResult = Tuple[MissingDict, ConflictingDict]30ConflictDetails = Tuple[PackageSet, CheckResult]313233def create_package_set_from_installed() -> Tuple[PackageSet, bool]:34"""Converts a list of distributions into a PackageSet."""35package_set = {}36problems = False37env = get_default_environment()38for dist in env.iter_installed_distributions(local_only=False, skip=()):39name = dist.canonical_name40try:41dependencies = list(dist.iter_dependencies())42package_set[name] = PackageDetails(dist.version, dependencies)43except (OSError, ValueError) as e:44# Don't crash on unreadable or broken metadata.45logger.warning("Error parsing requirements for %s: %s", name, e)46problems = True47return package_set, problems484950def check_package_set(51package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None52) -> CheckResult:53"""Check if a package set is consistent5455If should_ignore is passed, it should be a callable that takes a56package name and returns a boolean.57"""5859missing = {}60conflicting = {}6162for package_name, package_detail in package_set.items():63# Info about dependencies of package_name64missing_deps: Set[Missing] = set()65conflicting_deps: Set[Conflicting] = set()6667if should_ignore and should_ignore(package_name):68continue6970for req in package_detail.dependencies:71name = canonicalize_name(req.name)7273# Check if it's missing74if name not in package_set:75missed = True76if req.marker is not None:77missed = req.marker.evaluate()78if missed:79missing_deps.add((name, req))80continue8182# Check if there's a conflict83version = package_set[name].version84if not req.specifier.contains(version, prereleases=True):85conflicting_deps.add((name, version, req))8687if missing_deps:88missing[package_name] = sorted(missing_deps, key=str)89if conflicting_deps:90conflicting[package_name] = sorted(conflicting_deps, key=str)9192return missing, conflicting939495def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails:96"""For checking if the dependency graph would be consistent after \97installing given requirements98"""99# Start from the current state100package_set, _ = create_package_set_from_installed()101# Install packages102would_be_installed = _simulate_installation_of(to_install, package_set)103104# Only warn about directly-dependent packages; create a whitelist of them105whitelist = _create_whitelist(would_be_installed, package_set)106107return (108package_set,109check_package_set(110package_set, should_ignore=lambda name: name not in whitelist111),112)113114115def _simulate_installation_of(116to_install: List[InstallRequirement], package_set: PackageSet117) -> Set[NormalizedName]:118"""Computes the version of packages after installing to_install."""119# Keep track of packages that were installed120installed = set()121122# Modify it as installing requirement_set would (assuming no errors)123for inst_req in to_install:124abstract_dist = make_distribution_for_install_requirement(inst_req)125dist = abstract_dist.get_metadata_distribution()126name = dist.canonical_name127package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))128129installed.add(name)130131return installed132133134def _create_whitelist(135would_be_installed: Set[NormalizedName], package_set: PackageSet136) -> Set[NormalizedName]:137packages_affected = set(would_be_installed)138139for package_name in package_set:140if package_name in packages_affected:141continue142143for req in package_set[package_name].dependencies:144if canonicalize_name(req.name) in packages_affected:145packages_affected.add(package_name)146break147148return packages_affected149150151