图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

187 lines
6.6 KiB

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