1 """Validation of dependencies of packages
5 from typing
import Callable
, Dict
, List
, NamedTuple
, Optional
, Set
, Tuple
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
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
18 logger
= logging
.getLogger(__name__
)
21 class PackageDetails(NamedTuple
):
22 version
: DistributionVersion
23 dependencies
: List
[Requirement
]
27 PackageSet
= Dict
[NormalizedName
, PackageDetails
]
28 Missing
= Tuple
[NormalizedName
, Requirement
]
29 Conflicting
= Tuple
[NormalizedName
, DistributionVersion
, Requirement
]
31 MissingDict
= Dict
[NormalizedName
, List
[Missing
]]
32 ConflictingDict
= Dict
[NormalizedName
, List
[Conflicting
]]
33 CheckResult
= Tuple
[MissingDict
, ConflictingDict
]
34 ConflictDetails
= Tuple
[PackageSet
, CheckResult
]
37 def create_package_set_from_installed() -> Tuple
[PackageSet
, bool]:
38 """Converts a list of distributions into a PackageSet."""
41 env
= get_default_environment()
42 for dist
in env
.iter_installed_distributions(local_only
=False, skip
=()):
43 name
= dist
.canonical_name
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
)
51 return package_set
, problems
54 def check_package_set(
55 package_set
: PackageSet
, should_ignore
: Optional
[Callable
[[str], bool]] = None
57 """Check if a package set is consistent
59 If should_ignore is passed, it should be a callable that takes a
60 package name and returns a boolean.
63 warn_legacy_versions_and_specifiers(package_set
)
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()
73 if should_ignore
and should_ignore(package_name
):
76 for req
in package_detail
.dependencies
:
77 name
= canonicalize_name(req
.name
)
79 # Check if it's missing
80 if name
not in package_set
:
82 if req
.marker
is not None:
83 missed
= req
.marker
.evaluate({"extra": ""}
)
85 missing_deps
.add((name
, req
))
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
))
94 missing
[package_name
] = sorted(missing_deps
, key
=str)
96 conflicting
[package_name
] = sorted(conflicting_deps
, key
=str)
98 return missing
, conflicting
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
105 # Start from the current state
106 package_set
, _
= create_package_set_from_installed()
108 would_be_installed
= _simulate_installation_of(to_install
, package_set
)
110 # Only warn about directly-dependent packages; create a whitelist of them
111 whitelist
= _create_whitelist(would_be_installed
, package_set
)
116 package_set
, should_ignore
=lambda name
: name
not in whitelist
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
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()))
140 def _create_whitelist(
141 would_be_installed
: Set
[NormalizedName
], package_set
: PackageSet
142 ) -> Set
[NormalizedName
]:
143 packages_affected
= set(would_be_installed
)
145 for package_name
in package_set
:
146 if package_name
in packages_affected
:
149 for req
in package_set
[package_name
].dependencies
:
150 if canonicalize_name(req
.name
) in packages_affected
:
151 packages_affected
.add(package_name
)
154 return packages_affected
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
):
162 f
"{project_name} {package_details.version} "
163 f
"has a non-standard version number."
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"
173 for dep
in package_details
.dependencies
:
174 if any(isinstance(spec
, LegacySpecifier
) for spec
in dep
.specifier
):
177 f
"{project_name} {package_details.version} "
178 f
"has a non-standard dependency specifier {dep}."
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"