mirror of https://github.com/pypa/pip
Implement RequiresPythonRequirement with context
This specialized class is able to carry more context information than the previous implementation (which reuses ExplicitRequirement). Error reports can thus provide better messages by introspecting.
This commit is contained in:
parent
4e74a735d2
commit
d2028e9538
|
@ -1,5 +1,9 @@
|
|||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pip._internal.exceptions import (
|
||||
InstallationError,
|
||||
UnsupportedPythonVersion,
|
||||
)
|
||||
from pip._internal.utils.misc import get_installed_distributions
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
|
@ -12,7 +16,7 @@ from .candidates import (
|
|||
)
|
||||
from .requirements import (
|
||||
ExplicitRequirement,
|
||||
NoMatchRequirement,
|
||||
RequiresPythonRequirement,
|
||||
SpecifierRequirement,
|
||||
)
|
||||
|
||||
|
@ -22,6 +26,7 @@ if MYPY_CHECK_RUNNING:
|
|||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
from pip._vendor.packaging.version import _BaseVersion
|
||||
from pip._vendor.pkg_resources import Distribution
|
||||
from pip._vendor.resolvelib import ResolutionImpossible
|
||||
|
||||
from pip._internal.index.package_finder import PackageFinder
|
||||
from pip._internal.models.candidate import InstallationCandidate
|
||||
|
@ -152,16 +157,36 @@ class Factory(object):
|
|||
# type: (Optional[SpecifierSet]) -> Optional[Requirement]
|
||||
if self._ignore_requires_python or specifier is None:
|
||||
return None
|
||||
# The logic here is different from SpecifierRequirement, for which we
|
||||
# "find" candidates matching the specifier. But for Requires-Python,
|
||||
# there is always exactly one candidate (the one specified with
|
||||
# py_version_info). Here we decide whether to return that based on
|
||||
# whether Requires-Python matches that one candidate or not.
|
||||
if self._python_candidate.version in specifier:
|
||||
return ExplicitRequirement(self._python_candidate)
|
||||
return NoMatchRequirement(self._python_candidate.name)
|
||||
return RequiresPythonRequirement(specifier, self._python_candidate)
|
||||
|
||||
def should_reinstall(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
# TODO: Are there more cases this needs to return True? Editable?
|
||||
return candidate.name in self._installed_dists
|
||||
|
||||
def _report_requires_python_error(
|
||||
self,
|
||||
requirement, # type: RequiresPythonRequirement
|
||||
parent, # type: Candidate
|
||||
):
|
||||
# type: (...) -> UnsupportedPythonVersion
|
||||
template = (
|
||||
"Package {package!r} requires a different Python: "
|
||||
"{version} not in {specifier!r}"
|
||||
)
|
||||
message = template.format(
|
||||
package=parent.name,
|
||||
version=self._python_candidate.version,
|
||||
specifier=str(requirement.specifier),
|
||||
)
|
||||
return UnsupportedPythonVersion(message)
|
||||
|
||||
def get_installation_error(self, e):
|
||||
# type: (ResolutionImpossible) -> Optional[InstallationError]
|
||||
for cause in e.causes:
|
||||
if isinstance(cause.requirement, RequiresPythonRequirement):
|
||||
return self._report_requires_python_error(
|
||||
cause.requirement,
|
||||
cause.parent,
|
||||
)
|
||||
return None
|
||||
|
|
|
@ -7,6 +7,8 @@ from .base import Requirement, format_name
|
|||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Sequence
|
||||
|
||||
from pip._vendor.packaging.specifiers import SpecifierSet
|
||||
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
|
||||
from .base import Candidate
|
||||
|
@ -40,37 +42,6 @@ class ExplicitRequirement(Requirement):
|
|||
return candidate == self.candidate
|
||||
|
||||
|
||||
class NoMatchRequirement(Requirement):
|
||||
"""A requirement that never matches anything.
|
||||
|
||||
Note: Similar to ExplicitRequirement, the caller should handle name
|
||||
canonicalisation; this class does not perform it.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
# type: (str) -> None
|
||||
self._name = name
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}(name={name!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
name=self._name,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
return self._name
|
||||
|
||||
def find_matches(self):
|
||||
# type: () -> Sequence[Candidate]
|
||||
return []
|
||||
|
||||
def is_satisfied_by(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
return False
|
||||
|
||||
|
||||
class SpecifierRequirement(Requirement):
|
||||
def __init__(self, ireq, factory):
|
||||
# type: (InstallRequirement, Factory) -> None
|
||||
|
@ -114,3 +85,35 @@ class SpecifierRequirement(Requirement):
|
|||
"Internal issue: Candidate is not for this requirement " \
|
||||
" {} vs {}".format(candidate.name, self.name)
|
||||
return candidate.version in self._ireq.req.specifier
|
||||
|
||||
|
||||
class RequiresPythonRequirement(Requirement):
|
||||
"""A requirement representing Requires-Python metadata.
|
||||
"""
|
||||
def __init__(self, specifier, match):
|
||||
# type: (SpecifierSet, Candidate) -> None
|
||||
self.specifier = specifier
|
||||
self._candidate = match
|
||||
|
||||
def __repr__(self):
|
||||
# type: () -> str
|
||||
return "{class_name}({specifier!r})".format(
|
||||
class_name=self.__class__.__name__,
|
||||
specifier=str(self.specifier),
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
return self._candidate.name
|
||||
|
||||
def find_matches(self):
|
||||
# type: () -> Sequence[Candidate]
|
||||
if self._candidate.version in self.specifier:
|
||||
return [self._candidate]
|
||||
return []
|
||||
|
||||
def is_satisfied_by(self, candidate):
|
||||
# type: (Candidate) -> bool
|
||||
assert candidate.name == self._candidate.name, "Not Python candidate"
|
||||
return candidate.version in self.specifier
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import functools
|
||||
|
||||
from pip._vendor import six
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.resolvelib import BaseReporter
|
||||
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
|
||||
from pip._vendor.resolvelib import Resolver as RLResolver
|
||||
|
||||
from pip._internal.req.req_set import RequirementSet
|
||||
|
@ -64,7 +65,14 @@ class Resolver(BaseResolver):
|
|||
self.factory.make_requirement_from_install_req(r)
|
||||
for r in root_reqs
|
||||
]
|
||||
self._result = resolver.resolve(requirements)
|
||||
|
||||
try:
|
||||
self._result = resolver.resolve(requirements)
|
||||
except ResolutionImpossible as e:
|
||||
error = self.factory.get_installation_error(e)
|
||||
if not error:
|
||||
raise
|
||||
six.raise_from(error, e)
|
||||
|
||||
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
|
||||
for candidate in self._result.mapping.values():
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -230,6 +231,28 @@ def test_new_resolver_requires_python(
|
|||
assert_installed(script, base="0.1.0", dep=dep_version)
|
||||
|
||||
|
||||
def test_new_resolver_requires_python_error(script):
|
||||
create_basic_wheel_for_package(
|
||||
script,
|
||||
"base",
|
||||
"0.1.0",
|
||||
requires_python="<2",
|
||||
)
|
||||
result = script.pip(
|
||||
"install", "--unstable-feature=resolver",
|
||||
"--no-cache-dir", "--no-index",
|
||||
"--find-links", script.scratch_path,
|
||||
"base",
|
||||
expect_error=True,
|
||||
)
|
||||
|
||||
message = (
|
||||
"Package 'base' requires a different Python: "
|
||||
"{}.{}.{} not in '<2'".format(*sys.version_info[:3])
|
||||
)
|
||||
assert message in result.stderr, str(result)
|
||||
|
||||
|
||||
def test_new_resolver_installed(script):
|
||||
create_basic_wheel_for_package(
|
||||
script,
|
||||
|
|
Loading…
Reference in New Issue