python (3.11.7)
       1  """Validation of dependencies of packages
       2  """
       3  
       4  import logging
       5  from typing import Callable, Dict, List, NamedTuple, Optional, Set, Tuple
       6  
       7  from pip._vendor.packaging.requirements import Requirement
       8  from pip._vendor.packaging.specifiers import LegacySpecifier
       9  from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
      10  from pip._vendor.packaging.version import LegacyVersion
      11  
      12  from pip._internal.distributions import make_distribution_for_install_requirement
      13  from pip._internal.metadata import get_default_environment
      14  from pip._internal.metadata.base import DistributionVersion
      15  from pip._internal.req.req_install import InstallRequirement
      16  from pip._internal.utils.deprecation import deprecated
      17  
      18  logger = logging.getLogger(__name__)
      19  
      20  
      21  class ESC[4;38;5;81mPackageDetails(ESC[4;38;5;149mNamedTuple):
      22      version: DistributionVersion
      23      dependencies: List[Requirement]
      24  
      25  
      26  # Shorthands
      27  PackageSet = Dict[NormalizedName, PackageDetails]
      28  Missing = Tuple[NormalizedName, Requirement]
      29  Conflicting = Tuple[NormalizedName, DistributionVersion, Requirement]
      30  
      31  MissingDict = Dict[NormalizedName, List[Missing]]
      32  ConflictingDict = Dict[NormalizedName, List[Conflicting]]
      33  CheckResult = Tuple[MissingDict, ConflictingDict]
      34  ConflictDetails = Tuple[PackageSet, CheckResult]
      35  
      36  
      37  def create_package_set_from_installed() -> Tuple[PackageSet, bool]:
      38      """Converts a list of distributions into a PackageSet."""
      39      package_set = {}
      40      problems = False
      41      env = get_default_environment()
      42      for dist in env.iter_installed_distributions(local_only=False, skip=()):
      43          name = dist.canonical_name
      44          try:
      45              dependencies = list(dist.iter_dependencies())
      46              package_set[name] = PackageDetails(dist.version, dependencies)
      47          except (OSError, ValueError) as e:
      48              # Don't crash on unreadable or broken metadata.
      49              logger.warning("Error parsing requirements for %s: %s", name, e)
      50              problems = True
      51      return package_set, problems
      52  
      53  
      54  def check_package_set(
      55      package_set: PackageSet, should_ignore: Optional[Callable[[str], bool]] = None
      56  ) -> CheckResult:
      57      """Check if a package set is consistent
      58  
      59      If should_ignore is passed, it should be a callable that takes a
      60      package name and returns a boolean.
      61      """
      62  
      63      warn_legacy_versions_and_specifiers(package_set)
      64  
      65      missing = {}
      66      conflicting = {}
      67  
      68      for package_name, package_detail in package_set.items():
      69          # Info about dependencies of package_name
      70          missing_deps: Set[Missing] = set()
      71          conflicting_deps: Set[Conflicting] = set()
      72  
      73          if should_ignore and should_ignore(package_name):
      74              continue
      75  
      76          for req in package_detail.dependencies:
      77              name = canonicalize_name(req.name)
      78  
      79              # Check if it's missing
      80              if name not in package_set:
      81                  missed = True
      82                  if req.marker is not None:
      83                      missed = req.marker.evaluate({"extra": ""})
      84                  if missed:
      85                      missing_deps.add((name, req))
      86                  continue
      87  
      88              # Check if there's a conflict
      89              version = package_set[name].version
      90              if not req.specifier.contains(version, prereleases=True):
      91                  conflicting_deps.add((name, version, req))
      92  
      93          if missing_deps:
      94              missing[package_name] = sorted(missing_deps, key=str)
      95          if conflicting_deps:
      96              conflicting[package_name] = sorted(conflicting_deps, key=str)
      97  
      98      return missing, conflicting
      99  
     100  
     101  def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDetails:
     102      """For checking if the dependency graph would be consistent after \
     103      installing given requirements
     104      """
     105      # Start from the current state
     106      package_set, _ = create_package_set_from_installed()
     107      # Install packages
     108      would_be_installed = _simulate_installation_of(to_install, package_set)
     109  
     110      # Only warn about directly-dependent packages; create a whitelist of them
     111      whitelist = _create_whitelist(would_be_installed, package_set)
     112  
     113      return (
     114          package_set,
     115          check_package_set(
     116              package_set, should_ignore=lambda name: name not in whitelist
     117          ),
     118      )
     119  
     120  
     121  def _simulate_installation_of(
     122      to_install: List[InstallRequirement], package_set: PackageSet
     123  ) -> Set[NormalizedName]:
     124      """Computes the version of packages after installing to_install."""
     125      # Keep track of packages that were installed
     126      installed = set()
     127  
     128      # Modify it as installing requirement_set would (assuming no errors)
     129      for inst_req in to_install:
     130          abstract_dist = make_distribution_for_install_requirement(inst_req)
     131          dist = abstract_dist.get_metadata_distribution()
     132          name = dist.canonical_name
     133          package_set[name] = PackageDetails(dist.version, list(dist.iter_dependencies()))
     134  
     135          installed.add(name)
     136  
     137      return installed
     138  
     139  
     140  def _create_whitelist(
     141      would_be_installed: Set[NormalizedName], package_set: PackageSet
     142  ) -> Set[NormalizedName]:
     143      packages_affected = set(would_be_installed)
     144  
     145      for package_name in package_set:
     146          if package_name in packages_affected:
     147              continue
     148  
     149          for req in package_set[package_name].dependencies:
     150              if canonicalize_name(req.name) in packages_affected:
     151                  packages_affected.add(package_name)
     152                  break
     153  
     154      return packages_affected
     155  
     156  
     157  def warn_legacy_versions_and_specifiers(package_set: PackageSet) -> None:
     158      for project_name, package_details in package_set.items():
     159          if isinstance(package_details.version, LegacyVersion):
     160              deprecated(
     161                  reason=(
     162                      f"{project_name} {package_details.version} "
     163                      f"has a non-standard version number."
     164                  ),
     165                  replacement=(
     166                      f"to upgrade to a newer version of {project_name} "
     167                      f"or contact the author to suggest that they "
     168                      f"release a version with a conforming version number"
     169                  ),
     170                  issue=12063,
     171                  gone_in="23.3",
     172              )
     173          for dep in package_details.dependencies:
     174              if any(isinstance(spec, LegacySpecifier) for spec in dep.specifier):
     175                  deprecated(
     176                      reason=(
     177                          f"{project_name} {package_details.version} "
     178                          f"has a non-standard dependency specifier {dep}."
     179                      ),
     180                      replacement=(
     181                          f"to upgrade to a newer version of {project_name} "
     182                          f"or contact the author to suggest that they "
     183                          f"release a version with a conforming dependency specifiers"
     184                      ),
     185                      issue=12063,
     186                      gone_in="23.3",
     187                  )