Change venv

This commit is contained in:
Ambulance Clerc
2023-05-31 08:31:22 +02:00
parent fb6f579089
commit fdbb52c96f
466 changed files with 25899 additions and 64721 deletions

View File

@@ -1,9 +1,11 @@
from typing import Callable, List
from typing import Callable, List, Optional
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_set import RequirementSet
InstallRequirementProvider = Callable[[str, InstallRequirement], InstallRequirement]
InstallRequirementProvider = Callable[
[str, Optional[InstallRequirement]], InstallRequirement
]
class BaseResolver:

View File

@@ -20,7 +20,7 @@ from itertools import chain
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
from pip._vendor.packaging import specifiers
from pip._vendor.pkg_resources import Distribution
from pip._vendor.packaging.requirements import Requirement
from pip._internal.cache import WheelCache
from pip._internal.exceptions import (
@@ -28,10 +28,14 @@ from pip._internal.exceptions import (
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,
@@ -39,10 +43,12 @@ from pip._internal.req.req_install import (
)
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 dist_in_usersite, normalize_version_info
from pip._internal.utils.packaging import check_requires_python, get_requires_python
from pip._internal.utils.misc import normalize_version_info
from pip._internal.utils.packaging import check_requires_python
logger = logging.getLogger(__name__)
@@ -50,7 +56,7 @@ DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]]
def _check_dist_requires_python(
dist: Distribution,
dist: BaseDistribution,
version_info: Tuple[int, int, int],
ignore_requires_python: bool = False,
) -> None:
@@ -66,14 +72,21 @@ def _check_dist_requires_python(
:raises UnsupportedPythonVersion: When the given Python version isn't
compatible.
"""
requires_python = get_requires_python(dist)
# 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
requires_python,
version_info=version_info,
)
except specifiers.InvalidSpecifier as exc:
logger.warning(
"Package %r has an invalid Requires-Python: %s", dist.project_name, exc
"Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
)
return
@@ -84,7 +97,7 @@ def _check_dist_requires_python(
if ignore_requires_python:
logger.debug(
"Ignoring failed Requires-Python check for package %r: %s not in %r",
dist.project_name,
dist.raw_name,
version,
requires_python,
)
@@ -92,7 +105,7 @@ def _check_dist_requires_python(
raise UnsupportedPythonVersion(
"Package {!r} requires a different Python: {} not in {!r}".format(
dist.project_name, version, requires_python
dist.raw_name, version, requires_python
)
)
@@ -159,7 +172,7 @@ class Resolver(BaseResolver):
for req in root_reqs:
if req.constraint:
check_invalid_constraint_type(req)
requirement_set.add_requirement(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
@@ -179,6 +192,124 @@ class Resolver(BaseResolver):
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
@@ -194,7 +325,7 @@ class Resolver(BaseResolver):
"""
# 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 dist_in_usersite(req.satisfied_by):
if not self.use_user_site or req.satisfied_by.in_usersite:
req.should_reinstall = True
req.satisfied_by = None
@@ -300,10 +431,18 @@ class Resolver(BaseResolver):
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
req.cached_wheel_source_link = req.link
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.hashes 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) -> Distribution:
def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:
"""Takes a InstallRequirement and returns a single AbstractDist \
representing a prepared variant of the same.
"""
@@ -378,13 +517,14 @@ class Resolver(BaseResolver):
more_reqs: List[InstallRequirement] = []
def add_req(subreq: Distribution, extras_requested: Iterable[str]) -> None:
sub_install_req = self._make_install_req(
str(subreq),
req_to_install,
)
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 = requirement_set.add_requirement(
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,
@@ -401,7 +541,9 @@ class Resolver(BaseResolver):
# 'unnamed' requirements can only come from being directly
# provided by the user.
assert req_to_install.user_supplied
requirement_set.add_requirement(req_to_install, parent_req_name=None)
self._add_requirement_to_set(
requirement_set, req_to_install, parent_req_name=None
)
if not self.ignore_dependencies:
if req_to_install.extras:
@@ -410,15 +552,20 @@ class Resolver(BaseResolver):
",".join(req_to_install.extras),
)
missing_requested = sorted(
set(req_to_install.extras) - set(dist.extras)
set(req_to_install.extras) - set(dist.iter_provided_extras())
)
for missing in missing_requested:
logger.warning("%s does not provide the extra '%s'", dist, missing)
logger.warning(
"%s %s does not provide the extra '%s'",
dist.raw_name,
dist.version,
missing,
)
available_requested = sorted(
set(dist.extras) & set(req_to_install.extras)
set(dist.iter_provided_extras()) & set(req_to_install.extras)
)
for subreq in dist.requires(available_requested):
for subreq in dist.iter_dependencies(available_requested):
add_req(subreq, extras_requested=available_requested)
return more_reqs

View File

@@ -36,11 +36,8 @@ class Constraint:
links = frozenset([ireq.link]) if ireq.link else frozenset()
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
def __nonzero__(self) -> bool:
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __bool__(self) -> bool:
return self.__nonzero__()
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
def __and__(self, other: InstallRequirement) -> "Constraint":
if not isinstance(other, InstallRequirement):

View File

@@ -2,13 +2,15 @@ import logging
import sys
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pkg_resources import Distribution
from pip._internal.exceptions import HashError, MetadataInconsistent
from pip._internal.exceptions import (
HashError,
InstallationSubprocessError,
MetadataInconsistent,
)
from pip._internal.metadata import BaseDistribution
from pip._internal.models.link import Link, links_equivalent
from pip._internal.models.wheel import Wheel
from pip._internal.req.constructors import (
@@ -16,8 +18,8 @@ from pip._internal.req.constructors import (
install_req_from_line,
)
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.misc import dist_is_editable, normalize_version_info
from pip._internal.utils.packaging import get_requires_python
from pip._internal.utils.direct_url_helpers import direct_url_from_link
from pip._internal.utils.misc import normalize_version_info
from .base import Candidate, CandidateVersion, Requirement, format_name
@@ -63,14 +65,13 @@ def make_install_req_from_link(
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),
global_options=template.global_options,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
ireq.original_link = template.original_link
ireq.link = link
ireq.extras = template.extras
return ireq
@@ -78,31 +79,31 @@ def make_install_req_from_editable(
link: Link, template: InstallRequirement
) -> InstallRequirement:
assert template.editable, "template not editable"
return install_req_from_editable(
ireq = install_req_from_editable(
link.url,
user_supplied=template.user_supplied,
comes_from=template.comes_from,
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),
permit_editable_wheels=template.permit_editable_wheels,
global_options=template.global_options,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
ireq.extras = template.extras
return ireq
def make_install_req_from_dist(
dist: Distribution, template: InstallRequirement
def _make_install_req_from_dist(
dist: BaseDistribution, template: InstallRequirement
) -> InstallRequirement:
project_name = canonicalize_name(dist.project_name)
if template.req:
line = str(template.req)
elif template.link:
line = f"{project_name} @ {template.link.url}"
line = f"{dist.canonical_name} @ {template.link.url}"
else:
line = f"{project_name}=={dist.parsed_version}"
line = f"{dist.canonical_name}=={dist.version}"
ireq = install_req_from_line(
line,
user_supplied=template.user_supplied,
@@ -110,11 +111,9 @@ def make_install_req_from_dist(
use_pep517=template.use_pep517,
isolated=template.isolated,
constraint=template.constraint,
options=dict(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
),
global_options=template.global_options,
hash_options=template.hash_options,
config_settings=template.config_settings,
)
ireq.satisfied_by = dist
return ireq
@@ -136,6 +135,7 @@ class _InstallRequirementBackedCandidate(Candidate):
found remote link (e.g. from pypi.org).
"""
dist: BaseDistribution
is_installed = False
def __init__(
@@ -180,7 +180,7 @@ class _InstallRequirementBackedCandidate(Candidate):
def project_name(self) -> NormalizedName:
"""The normalised name of the project the candidate refers to"""
if self._name is None:
self._name = canonicalize_name(self.dist.project_name)
self._name = self.dist.canonical_name
return self._name
@property
@@ -190,7 +190,7 @@ class _InstallRequirementBackedCandidate(Candidate):
@property
def version(self) -> CandidateVersion:
if self._version is None:
self._version = parse_version(self.dist.version)
self._version = self.dist.version
return self._version
def format_for_error(self) -> str:
@@ -200,29 +200,27 @@ class _InstallRequirementBackedCandidate(Candidate):
self._link.file_path if self._link.is_file else self._link,
)
def _prepare_distribution(self) -> Distribution:
def _prepare_distribution(self) -> BaseDistribution:
raise NotImplementedError("Override in subclass")
def _check_metadata_consistency(self, dist: Distribution) -> None:
def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
"""Check for consistency of project name and version of dist."""
canonical_name = canonicalize_name(dist.project_name)
if self._name is not None and self._name != canonical_name:
if self._name is not None and self._name != dist.canonical_name:
raise MetadataInconsistent(
self._ireq,
"name",
self._name,
dist.project_name,
dist.canonical_name,
)
parsed_version = parse_version(dist.version)
if self._version is not None and self._version != parsed_version:
if self._version is not None and self._version != dist.version:
raise MetadataInconsistent(
self._ireq,
"version",
str(self._version),
dist.version,
str(dist.version),
)
def _prepare(self) -> Distribution:
def _prepare(self) -> BaseDistribution:
try:
dist = self._prepare_distribution()
except HashError as e:
@@ -231,26 +229,19 @@ class _InstallRequirementBackedCandidate(Candidate):
# offending line to the user.
e.req = self._ireq
raise
except InstallationSubprocessError as exc:
# The output has been presented already, so don't duplicate it.
exc.context = "See above for output."
raise
self._check_metadata_consistency(dist)
return dist
def _get_requires_python_dependency(self) -> Optional[Requirement]:
requires_python = get_requires_python(self.dist)
if requires_python is None:
return None
try:
spec = SpecifierSet(requires_python)
except InvalidSpecifier as e:
message = "Package %r has an invalid Requires-Python: %s"
logger.warning(message, self.name, e)
return None
return self._factory.make_requires_python_requirement(spec)
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
requires = self.dist.requires() if with_requires else ()
requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
yield self._get_requires_python_dependency()
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
def get_install_requirement(self) -> Optional[InstallRequirement]:
return self._ireq
@@ -268,7 +259,7 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
version: Optional[CandidateVersion] = None,
) -> None:
source_link = link
cache_entry = factory.get_wheel_cache_entry(link, name)
cache_entry = factory.get_wheel_cache_entry(source_link, name)
if cache_entry is not None:
logger.debug("Using cached wheel link: %s", cache_entry.link)
link = cache_entry.link
@@ -285,12 +276,19 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
version, wheel_version, name
)
if (
cache_entry is not None
and cache_entry.persistent
and template.link is template.original_link
):
ireq.original_link_is_in_wheel_cache = True
if cache_entry is not None:
assert ireq.link.is_wheel
assert ireq.link.is_file
if cache_entry.persistent and template.link is template.original_link:
ireq.cached_wheel_source_link = source_link
if cache_entry.origin is not None:
ireq.download_info = cache_entry.origin
else:
# Legacy cache entry that does not have origin.json.
# download_info may miss the archive_info.hashes field.
ireq.download_info = direct_url_from_link(
source_link, link_is_in_wheel_cache=cache_entry.persistent
)
super().__init__(
link=link,
@@ -301,10 +299,9 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
version=version,
)
def _prepare_distribution(self) -> Distribution:
return self._factory.preparer.prepare_linked_requirement(
self._ireq, parallel_builds=True
)
def _prepare_distribution(self) -> BaseDistribution:
preparer = self._factory.preparer
return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
class EditableCandidate(_InstallRequirementBackedCandidate):
@@ -327,7 +324,7 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
version=version,
)
def _prepare_distribution(self) -> Distribution:
def _prepare_distribution(self) -> BaseDistribution:
return self._factory.preparer.prepare_editable_requirement(self._ireq)
@@ -337,17 +334,17 @@ class AlreadyInstalledCandidate(Candidate):
def __init__(
self,
dist: Distribution,
dist: BaseDistribution,
template: InstallRequirement,
factory: "Factory",
) -> None:
self.dist = dist
self._ireq = make_install_req_from_dist(dist, template)
self._ireq = _make_install_req_from_dist(dist, template)
self._factory = factory
# This is just logging some messages, so we can do it eagerly.
# The returned dist would be exactly the same as self.dist because we
# set satisfied_by in make_install_req_from_dist.
# set satisfied_by in _make_install_req_from_dist.
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
skip_reason = "already satisfied"
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
@@ -371,7 +368,7 @@ class AlreadyInstalledCandidate(Candidate):
@property
def project_name(self) -> NormalizedName:
return canonicalize_name(self.dist.project_name)
return self.dist.canonical_name
@property
def name(self) -> str:
@@ -379,11 +376,11 @@ class AlreadyInstalledCandidate(Candidate):
@property
def version(self) -> CandidateVersion:
return parse_version(self.dist.version)
return self.dist.version
@property
def is_editable(self) -> bool:
return dist_is_editable(self.dist)
return self.dist.editable
def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"
@@ -391,7 +388,7 @@ class AlreadyInstalledCandidate(Candidate):
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
if not with_requires:
return
for r in self.dist.requires():
for r in self.dist.iter_dependencies():
yield self._factory.make_requirement_from_spec(str(r), self._ireq)
def get_install_requirement(self) -> Optional[InstallRequirement]:
@@ -491,8 +488,8 @@ class ExtrasCandidate(Candidate):
# The user may have specified extras that the candidate doesn't
# support. We ignore any unsupported extras here.
valid_extras = self.extras.intersection(self.base.dist.extras)
invalid_extras = self.extras.difference(self.base.dist.extras)
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
for extra in sorted(invalid_extras):
logger.warning(
"%s %s does not provide the extra '%s'",
@@ -501,7 +498,7 @@ class ExtrasCandidate(Candidate):
extra,
)
for r in self.base.dist.requires(valid_extras):
for r in self.base.dist.iter_dependencies(valid_extras):
requirement = factory.make_requirement_from_spec(
str(r), self.base._ireq, valid_extras
)

View File

@@ -19,7 +19,6 @@ from typing import (
)
from pip._vendor.packaging.requirements import InvalidRequirement
from pip._vendor.packaging.requirements import Requirement as PackagingRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.resolvelib import ResolutionImpossible
@@ -28,7 +27,6 @@ from pip._internal.cache import CacheEntry, WheelCache
from pip._internal.exceptions import (
DistributionNotFound,
InstallationError,
InstallationSubprocessError,
MetadataInconsistent,
UnsupportedPythonVersion,
UnsupportedWheel,
@@ -46,6 +44,7 @@ from pip._internal.req.req_install import (
from pip._internal.resolution.base import InstallRequirementProvider
from pip._internal.utils.compatibility_tags import get_supported
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.packaging import get_requirement
from pip._internal.utils.virtualenv import running_under_virtualenv
from .base import Candidate, CandidateVersion, Constraint, Requirement
@@ -158,10 +157,7 @@ class Factory:
try:
base = self._installed_candidate_cache[dist.canonical_name]
except KeyError:
from pip._internal.metadata.pkg_resources import Distribution as _Dist
compat_dist = cast(_Dist, dist)._dist
base = AlreadyInstalledCandidate(compat_dist, template, factory=self)
base = AlreadyInstalledCandidate(dist, template, factory=self)
self._installed_candidate_cache[dist.canonical_name] = base
if not extras:
return base
@@ -193,10 +189,16 @@ class Factory:
name=name,
version=version,
)
except (InstallationSubprocessError, MetadataInconsistent) as e:
logger.warning("Discarding %s. %s", link, e)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
e,
extra={"markup": True},
)
self._build_failures[link] = e
return None
base: BaseCandidate = self._editable_candidate_cache[link]
else:
if link not in self._link_candidate_cache:
@@ -208,8 +210,13 @@ class Factory:
name=name,
version=version,
)
except (InstallationSubprocessError, MetadataInconsistent) as e:
logger.warning("Discarding %s. %s", link, e)
except MetadataInconsistent as e:
logger.info(
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
link,
e,
extra={"markup": True},
)
self._build_failures[link] = e
return None
base = self._link_candidate_cache[link]
@@ -263,7 +270,7 @@ class Factory:
extras=extras,
template=template,
)
# The candidate is a known incompatiblity. Don't use it.
# The candidate is a known incompatibility. Don't use it.
if id(candidate) in incompatible_ids:
return None
return candidate
@@ -276,14 +283,27 @@ class Factory:
)
icans = list(result.iter_applicable())
# PEP 592: Yanked releases must be ignored unless only yanked
# releases can satisfy the version range. So if this is false,
# all yanked icans need to be skipped.
# PEP 592: Yanked releases are ignored unless the specifier
# explicitly pins a version (via '==' or '===') that can be
# solely satisfied by a yanked release.
all_yanked = all(ican.link.is_yanked for ican in icans)
def is_pinned(specifier: SpecifierSet) -> bool:
for sp in specifier:
if sp.operator == "===":
return True
if sp.operator != "==":
continue
if sp.version.endswith(".*"):
continue
return True
return False
pinned = is_pinned(specifier)
# PackageFinder returns earlier versions first, so we reverse.
for ican in reversed(icans):
if not all_yanked and ican.link.is_yanked:
if not (all_yanked and pinned) and ican.link.is_yanked:
continue
func = functools.partial(
self._make_candidate_from_link,
@@ -350,7 +370,7 @@ class Factory:
def find_candidates(
self,
identifier: str,
requirements: Mapping[str, Iterator[Requirement]],
requirements: Mapping[str, Iterable[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
constraint: Constraint,
prefers_installed: bool,
@@ -368,7 +388,7 @@ class Factory:
# If the current identifier contains extras, add explicit candidates
# from entries from extra-less identifier.
with contextlib.suppress(InvalidRequirement):
parsed_requirement = PackagingRequirement(identifier)
parsed_requirement = get_requirement(identifier)
explicit_candidates.update(
self._iter_explicit_candidates_from_base(
requirements.get(parsed_requirement.name, ()),
@@ -377,7 +397,7 @@ class Factory:
)
# Add explicit candidates from constraints. We only do this if there are
# kown ireqs, which represent requirements not already explicit. If
# known ireqs, which represent requirements not already explicit. If
# there are no ireqs, we're constraining already-explicit requirements,
# which is handled later when we return the explicit candidates.
if ireqs:
@@ -487,16 +507,20 @@ class Factory:
def make_requirement_from_spec(
self,
specifier: str,
comes_from: InstallRequirement,
comes_from: Optional[InstallRequirement],
requested_extras: Iterable[str] = (),
) -> Optional[Requirement]:
ireq = self._make_install_req_from_spec(specifier, comes_from)
return self._make_requirement_from_install_req(ireq, requested_extras)
def make_requires_python_requirement(
self, specifier: Optional[SpecifierSet]
self,
specifier: SpecifierSet,
) -> Optional[Requirement]:
if self._ignore_requires_python or specifier is None:
if self._ignore_requires_python:
return None
# Don't bother creating a dependency for an empty Requires-Python.
if not str(specifier):
return None
return RequiresPythonRequirement(specifier, self._python_candidate)
@@ -511,7 +535,7 @@ class Factory:
hash mismatches. Furthermore, cached wheels at present have
nondeterministic contents due to file modification times.
"""
if self._wheel_cache is None or self.preparer.require_hashes:
if self._wheel_cache is None:
return None
return self._wheel_cache.get_cache_entry(
link=link,
@@ -578,8 +602,15 @@ class Factory:
req_disp = f"{req} (from {parent.name})"
cands = self._finder.find_all_candidates(req.project_name)
skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
versions = [str(v) for v in sorted({c.version for c in cands})]
if skipped_by_requires_python:
logger.critical(
"Ignored the following versions that require a different python "
"version: %s",
"; ".join(skipped_by_requires_python) or "none",
)
logger.critical(
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
@@ -601,7 +632,6 @@ class Factory:
e: "ResolutionImpossible[Requirement, Candidate]",
constraints: Dict[str, Constraint],
) -> InstallationError:
assert e.causes, "Installation error reported with no cause"
# If one of the things we can't solve is "we need Python X.Y",
@@ -614,7 +644,7 @@ class Factory:
]
if requires_python_causes:
# The comprehension above makes sure all Requirement instances are
# RequiresPythonRequirement, so let's cast for convinience.
# RequiresPythonRequirement, so let's cast for convenience.
return self._report_requires_python_error(
cast("Sequence[ConflictCause]", requires_python_causes),
)
@@ -695,6 +725,6 @@ class Factory:
return DistributionNotFound(
"ResolutionImpossible: for help visit "
"https://pip.pypa.io/en/latest/user_guide/"
"#fixing-conflicting-dependencies"
"https://pip.pypa.io/en/latest/topics/dependency-resolution/"
"#dealing-with-dependency-conflicts"
)

View File

@@ -9,15 +9,30 @@ something.
"""
import functools
from typing import Callable, Iterator, Optional, Set, Tuple
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Set, Tuple
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.six.moves import collections_abc # type: ignore
from .base import Candidate
IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
if TYPE_CHECKING:
SequenceCandidate = Sequence[Candidate]
else:
# For compatibility: Python before 3.9 does not support using [] on the
# Sequence class.
#
# >>> from collections.abc import Sequence
# >>> Sequence[str]
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: 'ABCMeta' object is not subscriptable
#
# TODO: Remove this block after dropping Python 3.8 support.
SequenceCandidate = Sequence
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
"""Iterator for ``FoundCandidates``.
@@ -90,7 +105,7 @@ def _iter_built_with_inserted(
yield installed
class FoundCandidates(collections_abc.Sequence):
class FoundCandidates(SequenceCandidate):
"""A lazy sequence to provide candidates to the resolver.
The intended usage is to return this from `find_matches()` so the resolver
@@ -111,7 +126,7 @@ class FoundCandidates(collections_abc.Sequence):
self._prefers_installed = prefers_installed
self._incompatible_ids = incompatible_ids
def __getitem__(self, index: int) -> Candidate:
def __getitem__(self, index: Any) -> Any:
# Implemented to satisfy the ABC check. This is not needed by the
# resolver, and should not be used by the provider either (for
# performance reasons).
@@ -138,5 +153,3 @@ class FoundCandidates(collections_abc.Sequence):
if self._prefers_installed and self._installed:
return True
return any(self)
__nonzero__ = __bool__ # XXX: Python 2.

View File

@@ -1,6 +1,15 @@
import collections
import math
from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Mapping, Sequence, Union
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
Iterator,
Mapping,
Sequence,
TypeVar,
Union,
)
from pip._vendor.resolvelib.providers import AbstractProvider
@@ -37,6 +46,35 @@ else:
# services to those objects (access to pip's finder and preparer).
D = TypeVar("D")
V = TypeVar("V")
def _get_with_identifier(
mapping: Mapping[str, V],
identifier: str,
default: D,
) -> Union[D, V]:
"""Get item from a package name lookup mapping with a resolver identifier.
This extra logic is needed when the target mapping is keyed by package
name, which cannot be directly looked up with an identifier (which may
contain requested extras). Additional logic is added to also look up a value
by "cleaning up" the extras from the identifier.
"""
if identifier in mapping:
return mapping[identifier]
# HACK: Theoretically we should check whether this identifier is a valid
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
# some regular expression. But since pip's resolver only spits out three
# kinds of identifiers: normalized PEP 503 names, normalized names plus
# extras, and Requires-Python, we can cheat a bit here.
name, open_bracket, _ = identifier.partition("[")
if open_bracket and name in mapping:
return mapping[name]
return default
class PipProvider(_ProviderBase):
"""Pip's provider implementation for resolvelib.
@@ -71,28 +109,44 @@ class PipProvider(_ProviderBase):
identifier: str,
resolutions: Mapping[str, Candidate],
candidates: Mapping[str, Iterator[Candidate]],
information: Mapping[str, Iterator["PreferenceInformation"]],
information: Mapping[str, Iterable["PreferenceInformation"]],
backtrack_causes: Sequence["PreferenceInformation"],
) -> "Preference":
"""Produce a sort key for given requirement based on preference.
The lower the return value is, the more preferred this group of
arguments is.
Currently pip considers the followings in order:
Currently pip considers the following in order:
* Prefer if any of the known requirements is "direct", e.g. points to an
explicit URL.
* If equal, prefer if any requirement is "pinned", i.e. contains
operator ``===`` or ``==``.
* If equal, calculate an approximate "depth" and resolve requirements
closer to the user-specified requirements first.
closer to the user-specified requirements first. If the depth cannot
by determined (eg: due to no matching parents), it is considered
infinite.
* Order user-specified requirements by the order they are specified.
* If equal, prefers "non-free" requirements, i.e. contains at least one
operator, such as ``>=`` or ``<``.
* If equal, order alphabetically for consistency (helps debuggability).
"""
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
candidate, ireqs = zip(*lookups)
try:
next(iter(information[identifier]))
except StopIteration:
# There is no information for this identifier, so there's no known
# candidates.
has_information = False
else:
has_information = True
if has_information:
lookups = (r.get_candidate_lookup() for r, _ in information[identifier])
candidate, ireqs = zip(*lookups)
else:
candidate, ireqs = None, ()
operators = [
specifier.operator
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
@@ -107,14 +161,17 @@ class PipProvider(_ProviderBase):
requested_order: Union[int, float] = self._user_requested[identifier]
except KeyError:
requested_order = math.inf
parent_depths = (
self._known_depths[parent.name] if parent is not None else 0.0
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
self._known_depths[identifier] = inferred_depth
if has_information:
parent_depths = (
self._known_depths[parent.name] if parent is not None else 0.0
for _, parent in information[identifier]
)
inferred_depth = min(d for d in parent_depths) + 1.0
else:
inferred_depth = math.inf
else:
inferred_depth = 1.0
self._known_depths[identifier] = inferred_depth
requested_order = self._user_requested.get(identifier, math.inf)
@@ -122,49 +179,29 @@ class PipProvider(_ProviderBase):
# free, so we always do it first to avoid needless work if it fails.
requires_python = identifier == REQUIRES_PYTHON_IDENTIFIER
# HACK: Setuptools have a very long and solid backward compatibility
# track record, and extremely few projects would request a narrow,
# non-recent version range of it since that would break a lot things.
# (Most projects specify it only to request for an installer feature,
# which does not work, but that's another topic.) Intentionally
# delaying Setuptools helps reduce branches the resolver has to check.
# This serves as a temporary fix for issues like "apache-airlfow[all]"
# while we work on "proper" branch pruning techniques.
delay_this = identifier == "setuptools"
# Prefer the causes of backtracking on the assumption that the problem
# resolving the dependency tree is related to the failures that caused
# the backtracking
backtrack_cause = self.is_backtrack_cause(identifier, backtrack_causes)
return (
not requires_python,
delay_this,
not direct,
not pinned,
not backtrack_cause,
inferred_depth,
requested_order,
not unfree,
identifier,
)
def _get_constraint(self, identifier: str) -> Constraint:
if identifier in self._constraints:
return self._constraints[identifier]
# HACK: Theoratically we should check whether this identifier is a valid
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
# some regular expression. But since pip's resolver only spits out
# three kinds of identifiers: normalized PEP 503 names, normalized names
# plus extras, and Requires-Python, we can cheat a bit here.
name, open_bracket, _ = identifier.partition("[")
if open_bracket and name in self._constraints:
return self._constraints[name]
return Constraint.empty()
def find_matches(
self,
identifier: str,
requirements: Mapping[str, Iterator[Requirement]],
incompatibilities: Mapping[str, Iterator[Candidate]],
) -> Iterable[Candidate]:
def _eligible_for_upgrade(name: str) -> bool:
def _eligible_for_upgrade(identifier: str) -> bool:
"""Are upgrades allowed for this project?
This checks the upgrade strategy, and whether the project was one
@@ -178,13 +215,23 @@ class PipProvider(_ProviderBase):
if self._upgrade_strategy == "eager":
return True
elif self._upgrade_strategy == "only-if-needed":
return name in self._user_requested
user_order = _get_with_identifier(
self._user_requested,
identifier,
default=None,
)
return user_order is not None
return False
constraint = _get_with_identifier(
self._constraints,
identifier,
default=Constraint.empty(),
)
return self._factory.find_candidates(
identifier=identifier,
requirements=requirements,
constraint=self._get_constraint(identifier),
constraint=constraint,
prefers_installed=(not _eligible_for_upgrade(identifier)),
incompatibilities=incompatibilities,
)
@@ -195,3 +242,14 @@ class PipProvider(_ProviderBase):
def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
with_requires = not self._ignore_dependencies
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]
@staticmethod
def is_backtrack_cause(
identifier: str, backtrack_causes: Sequence["PreferenceInformation"]
) -> bool:
for backtrack_cause in backtrack_causes:
if identifier == backtrack_cause.requirement.name:
return True
if backtrack_cause.parent and identifier == backtrack_cause.parent.name:
return True
return False

View File

@@ -11,9 +11,9 @@ logger = getLogger(__name__)
class PipReporter(BaseReporter):
def __init__(self) -> None:
self.backtracks_by_package: DefaultDict[str, int] = defaultdict(int)
self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int)
self._messages_at_backtrack = {
self._messages_at_reject_count = {
1: (
"pip is looking at multiple versions of {package_name} to "
"determine which version is compatible with other "
@@ -27,22 +27,33 @@ class PipReporter(BaseReporter):
13: (
"This is taking longer than usual. You might need to provide "
"the dependency resolver with stricter constraints to reduce "
"runtime. If you want to abort this run, you can press "
"Ctrl + C to do so. To improve how pip performs, tell us what "
"happened here: https://pip.pypa.io/surveys/backtracking"
"runtime. See https://pip.pypa.io/warnings/backtracking for "
"guidance. If you want to abort this run, press Ctrl + C."
),
}
def backtracking(self, candidate: Candidate) -> None:
self.backtracks_by_package[candidate.name] += 1
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
self.reject_count_by_package[candidate.name] += 1
count = self.backtracks_by_package[candidate.name]
if count not in self._messages_at_backtrack:
count = self.reject_count_by_package[candidate.name]
if count not in self._messages_at_reject_count:
return
message = self._messages_at_backtrack[count]
message = self._messages_at_reject_count[count]
logger.info("INFO: %s", message.format(package_name=candidate.name))
msg = "Will try a different candidate, due to conflict:"
for req_info in criterion.information:
req, parent = req_info.requirement, req_info.parent
# Inspired by Factory.get_installation_error
msg += "\n "
if parent:
msg += f"{parent.name} {parent.version} depends on "
else:
msg += "The user requested "
msg += req.format_for_error()
logger.debug(msg)
class PipDebuggingReporter(BaseReporter):
"""A reporter that does an info log for every event it sees."""
@@ -62,8 +73,8 @@ class PipDebuggingReporter(BaseReporter):
def adding_requirement(self, requirement: Requirement, parent: Candidate) -> None:
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
def backtracking(self, candidate: Candidate) -> None:
logger.info("Reporter.backtracking(%r)", candidate)
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate)
def pinning(self, candidate: Candidate) -> None:
logger.info("Reporter.pinning(%r)", candidate)

View File

@@ -21,12 +21,12 @@ class ExplicitRequirement(Requirement):
@property
def project_name(self) -> NormalizedName:
# No need to canonicalise - the candidate did this
# No need to canonicalize - the candidate did this
return self.candidate.project_name
@property
def name(self) -> str:
# No need to canonicalise - the candidate did this
# No need to canonicalize - the candidate did this
return self.candidate.name
def format_for_error(self) -> str:
@@ -64,7 +64,6 @@ class SpecifierRequirement(Requirement):
return format_name(self.project_name, self._extras)
def format_for_error(self) -> str:
# Convert comma-separated specifiers into "A, B, ..., F and G"
# This makes the specifier a bit more "human readable", without
# risking a change in meaning. (Hopefully! Not all edge cases have

View File

@@ -19,8 +19,6 @@ from pip._internal.resolution.resolvelib.reporter import (
PipDebuggingReporter,
PipReporter,
)
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.filetypes import is_archive_file
from .base import Candidate, Requirement
from .factory import Factory
@@ -90,9 +88,9 @@ class Resolver(BaseResolver):
)
try:
try_to_avoid_resolution_too_deep = 2000000
limit_how_complex_resolution_can_be = 200000
result = self._result = resolver.resolve(
collected.requirements, max_rounds=try_to_avoid_resolution_too_deep
collected.requirements, max_rounds=limit_how_complex_resolution_can_be
)
except ResolutionImpossible as e:
@@ -136,25 +134,6 @@ class Resolver(BaseResolver):
)
continue
looks_like_sdist = (
is_archive_file(candidate.source_link.file_path)
and candidate.source_link.ext != ".zip"
)
if looks_like_sdist:
# is a local sdist -- show a deprecation warning!
reason = (
"Source distribution is being reinstalled despite an "
"installed package having the same name and version as "
"the installed package."
)
replacement = "use --force-reinstall"
deprecated(
reason=reason,
replacement=replacement,
gone_in="21.3",
issue=8711,
)
# is a local sdist or path -- reinstall
ireq.should_reinstall = True
else:
@@ -192,17 +171,19 @@ class Resolver(BaseResolver):
get installed one-by-one.
The current implementation creates a topological ordering of the
dependency graph, while breaking any cycles in the graph at arbitrary
points. We make no guarantees about where the cycle would be broken,
other than they would be broken.
dependency graph, giving more weight to packages with less
or no dependencies, while breaking any cycles in the graph at
arbitrary points. We make no guarantees about where the cycle
would be broken, other than it *would* be broken.
"""
assert self._result is not None, "must call resolve() first"
if not req_set.requirements:
# Nothing is left to install, so we do not need an order.
return []
graph = self._result.graph
weights = get_topological_weights(
graph,
expected_node_count=len(self._result.mapping) + 1,
)
weights = get_topological_weights(graph, set(req_set.requirements.keys()))
sorted_items = sorted(
req_set.requirements.items(),
@@ -213,23 +194,32 @@ class Resolver(BaseResolver):
def get_topological_weights(
graph: "DirectedGraph[Optional[str]]", expected_node_count: int
graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
) -> Dict[Optional[str], int]:
"""Assign weights to each node based on how "deep" they are.
This implementation may change at any point in the future without prior
notice.
We take the length for the longest path to any node from root, ignoring any
paths that contain a single node twice (i.e. cycles). This is done through
a depth-first search through the graph, while keeping track of the path to
the node.
We first simplify the dependency graph by pruning any leaves and giving them
the highest weight: a package without any dependencies should be installed
first. This is done again and again in the same way, giving ever less weight
to the newly found leaves. The loop stops when no leaves are left: all
remaining packages have at least one dependency left in the graph.
Then we continue with the remaining graph, by taking the length for the
longest path to any node from root, ignoring any paths that contain a single
node twice (i.e. cycles). This is done through a depth-first search through
the graph, while keeping track of the path to the node.
Cycles in the graph result would result in node being revisited while also
being it's own path. In this case, take no action. This helps ensure we
being on its own path. In this case, take no action. This helps ensure we
don't get stuck in a cycle.
When assigning weight, the longer path (i.e. larger length) is preferred.
We are only interested in the weights of packages that are in the
requirement_keys.
"""
path: Set[Optional[str]] = set()
weights: Dict[Optional[str], int] = {}
@@ -245,15 +235,49 @@ def get_topological_weights(
visit(child)
path.remove(node)
if node not in requirement_keys:
return
last_known_parent_count = weights.get(node, 0)
weights[node] = max(last_known_parent_count, len(path))
# Simplify the graph, pruning leaves that have no dependencies.
# This is needed for large graphs (say over 200 packages) because the
# `visit` function is exponentially slower then, taking minutes.
# See https://github.com/pypa/pip/issues/10557
# We will loop until we explicitly break the loop.
while True:
leaves = set()
for key in graph:
if key is None:
continue
for _child in graph.iter_children(key):
# This means we have at least one child
break
else:
# No child.
leaves.add(key)
if not leaves:
# We are done simplifying.
break
# Calculate the weight for the leaves.
weight = len(graph) - 1
for leaf in leaves:
if leaf not in requirement_keys:
continue
weights[leaf] = weight
# Remove the leaves from the graph, making it simpler.
for leaf in leaves:
graph.remove(leaf)
# Visit the remaining graph.
# `None` is guaranteed to be the root node by resolvelib.
visit(None)
# Sanity checks
assert weights[None] == 0
assert len(weights) == expected_node_count
# Sanity check: all requirement keys should be in the weights,
# and no other keys should be in the weights.
difference = set(weights.keys()).difference(requirement_keys)
assert not difference, difference
return weights