mirror of https://github.com/pypa/pip
Merge pull request #7897 from pfmoore/new_resolver_extras
New resolver extras implementation
This commit is contained in:
commit
c3d86200b0
|
@ -3,7 +3,7 @@ from pip._vendor.packaging.utils import canonicalize_name
|
|||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Sequence, Set
|
||||
from typing import Optional, Sequence, Set
|
||||
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._vendor.packaging.version import _BaseVersion
|
||||
|
@ -46,3 +46,7 @@ class Candidate(object):
|
|||
def get_dependencies(self):
|
||||
# type: () -> Sequence[InstallRequirement]
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
raise NotImplementedError("Override in subclass")
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import logging
|
||||
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
from pip._internal.req.constructors import install_req_from_line
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
from .base import Candidate
|
||||
from .base import Candidate, format_name
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
from typing import Any, Dict, Optional, Sequence
|
||||
from typing import Any, Dict, Optional, Sequence, Set
|
||||
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.operations.prepare import RequirementPreparer
|
||||
|
@ -16,15 +18,18 @@ if MYPY_CHECK_RUNNING:
|
|||
from pip._vendor.packaging.version import _BaseVersion
|
||||
from pip._vendor.pkg_resources import Distribution
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CANDIDATE_CACHE = {} # type: Dict[Link, Candidate]
|
||||
|
||||
_CANDIDATE_CACHE = {} # type: Dict[Link, LinkCandidate]
|
||||
|
||||
|
||||
def make_candidate(
|
||||
link, # type: Link
|
||||
preparer, # type: RequirementPreparer
|
||||
parent, # type: InstallRequirement
|
||||
make_install_req # type: InstallRequirementProvider
|
||||
link, # type: Link
|
||||
preparer, # type: RequirementPreparer
|
||||
parent, # type: InstallRequirement
|
||||
make_install_req, # type: InstallRequirementProvider
|
||||
extras # type: Set[str]
|
||||
):
|
||||
# type: (...) -> Candidate
|
||||
if link not in _CANDIDATE_CACHE:
|
||||
|
@ -34,7 +39,10 @@ def make_candidate(
|
|||
parent=parent,
|
||||
make_install_req=make_install_req
|
||||
)
|
||||
return _CANDIDATE_CACHE[link]
|
||||
base = _CANDIDATE_CACHE[link]
|
||||
if extras:
|
||||
return ExtrasCandidate(base, extras)
|
||||
return base
|
||||
|
||||
|
||||
def make_install_req_from_link(link, parent):
|
||||
|
@ -67,7 +75,10 @@ class LinkCandidate(Candidate):
|
|||
self.link = link
|
||||
self._preparer = preparer
|
||||
self._ireq = make_install_req_from_link(link, parent)
|
||||
self._make_install_req = make_install_req
|
||||
self._make_install_req = lambda spec: make_install_req(
|
||||
spec,
|
||||
self._ireq
|
||||
)
|
||||
|
||||
self._name = None # type: Optional[str]
|
||||
self._version = None # type: Optional[_BaseVersion]
|
||||
|
@ -120,7 +131,85 @@ class LinkCandidate(Candidate):
|
|||
|
||||
def get_dependencies(self):
|
||||
# type: () -> Sequence[InstallRequirement]
|
||||
return [
|
||||
self._make_install_req(str(r), self._ireq)
|
||||
for r in self.dist.requires()
|
||||
return [self._make_install_req(str(r)) for r in self.dist.requires()]
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
return self._ireq
|
||||
|
||||
|
||||
class ExtrasCandidate(LinkCandidate):
|
||||
"""A candidate that has 'extras', indicating additional dependencies.
|
||||
|
||||
Requirements can be for a project with dependencies, something like
|
||||
foo[extra]. The extras don't affect the project/version being installed
|
||||
directly, but indicate that we need additional dependencies. We model that
|
||||
by having an artificial ExtrasCandidate that wraps the "base" candidate.
|
||||
|
||||
The ExtrasCandidate differs from the base in the following ways:
|
||||
|
||||
1. It has a unique name, of the form foo[extra]. This causes the resolver
|
||||
to treat it as a separate node in the dependency graph.
|
||||
2. When we're getting the candidate's dependencies,
|
||||
a) We specify that we want the extra dependencies as well.
|
||||
b) We add a dependency on the base candidate (matching the name and
|
||||
version). See below for why this is needed.
|
||||
3. We return None for the underlying InstallRequirement, as the base
|
||||
candidate will provide it, and we don't want to end up with duplicates.
|
||||
|
||||
The dependency on the base candidate is needed so that the resolver can't
|
||||
decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
|
||||
version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
|
||||
respectively forces the resolver to recognise that this is a conflict.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
base, # type: LinkCandidate
|
||||
extras, # type: Set[str]
|
||||
):
|
||||
# type: (...) -> None
|
||||
self.base = base
|
||||
self.extras = extras
|
||||
self.link = base.link
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
"""The normalised name of the project the candidate refers to"""
|
||||
return format_name(self.base.name, self.extras)
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
# type: () -> _BaseVersion
|
||||
return self.base.version
|
||||
|
||||
def get_dependencies(self):
|
||||
# type: () -> Sequence[InstallRequirement]
|
||||
|
||||
# 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)
|
||||
if invalid_extras:
|
||||
logger.warning(
|
||||
"Invalid extras specified in %s: %s",
|
||||
self.name,
|
||||
','.join(sorted(invalid_extras))
|
||||
)
|
||||
|
||||
deps = [
|
||||
self.base._make_install_req(str(r))
|
||||
for r in self.base.dist.requires(valid_extras)
|
||||
]
|
||||
# Add a dependency on the exact base.
|
||||
# (See note 2b in the class docstring)
|
||||
spec = "{}=={}".format(self.base.name, self.base.version)
|
||||
deps.append(self.base._make_install_req(spec))
|
||||
return deps
|
||||
|
||||
def get_install_requirement(self):
|
||||
# type: () -> Optional[InstallRequirement]
|
||||
# We don't return anything here, because we always
|
||||
# depend on the base candidate, and we'll get the
|
||||
# install requirement from that.
|
||||
return None
|
||||
|
|
|
@ -39,16 +39,8 @@ class PipProvider(AbstractProvider):
|
|||
)
|
||||
|
||||
def get_install_requirement(self, c):
|
||||
# type: (Candidate) -> InstallRequirement
|
||||
|
||||
# The base Candidate class does not have an _ireq attribute, so we
|
||||
# fetch it dynamically here, to satisfy mypy. In practice, though, we
|
||||
# only ever deal with LinkedCandidate objects at the moment, which do
|
||||
# have an _ireq attribute. When we have a candidate type for installed
|
||||
# requirements we should probably review this.
|
||||
#
|
||||
# TODO: Longer term, make a proper interface for this on the candidate.
|
||||
return getattr(c, "_ireq", None)
|
||||
# type: (Candidate) -> Optional[InstallRequirement]
|
||||
return c.get_install_requirement()
|
||||
|
||||
def identify(self, dependency):
|
||||
# type: (Union[Requirement, Candidate]) -> str
|
||||
|
|
|
@ -2,7 +2,7 @@ from pip._vendor.packaging.utils import canonicalize_name
|
|||
|
||||
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
||||
|
||||
from .base import Requirement
|
||||
from .base import Requirement, format_name
|
||||
from .candidates import make_candidate
|
||||
|
||||
if MYPY_CHECK_RUNNING:
|
||||
|
@ -28,7 +28,8 @@ def make_requirement(
|
|||
ireq.link,
|
||||
preparer,
|
||||
ireq,
|
||||
make_install_req
|
||||
make_install_req,
|
||||
set()
|
||||
)
|
||||
return ExplicitRequirement(candidate)
|
||||
else:
|
||||
|
@ -70,17 +71,17 @@ class SpecifierRequirement(Requirement):
|
|||
):
|
||||
# type: (...) -> None
|
||||
assert ireq.link is None, "This is a link, not a specifier"
|
||||
assert not ireq.req.extras, "Extras not yet supported"
|
||||
self._ireq = ireq
|
||||
self._finder = finder
|
||||
self._preparer = preparer
|
||||
self._make_install_req = make_install_req
|
||||
self.extras = ireq.req.extras
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
# type: () -> str
|
||||
canonical_name = canonicalize_name(self._ireq.req.name)
|
||||
return canonical_name
|
||||
return format_name(canonical_name, self.extras)
|
||||
|
||||
def find_matches(self):
|
||||
# type: () -> Sequence[Candidate]
|
||||
|
@ -94,7 +95,8 @@ class SpecifierRequirement(Requirement):
|
|||
ican.link,
|
||||
self._preparer,
|
||||
self._ireq,
|
||||
self._make_install_req
|
||||
self._make_install_req,
|
||||
self.extras
|
||||
)
|
||||
for ican in found.iter_applicable()
|
||||
]
|
||||
|
|
|
@ -58,7 +58,8 @@ class Resolver(BaseResolver):
|
|||
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
|
||||
for candidate in self._result.mapping.values():
|
||||
ireq = provider.get_install_requirement(candidate)
|
||||
req_set.add_named_requirement(ireq)
|
||||
if ireq is not None:
|
||||
req_set.add_named_requirement(ireq)
|
||||
|
||||
return req_set
|
||||
|
||||
|
|
|
@ -111,3 +111,27 @@ def test_new_resolver_ignore_dependencies(script):
|
|||
)
|
||||
assert_installed(script, base="0.1.0")
|
||||
assert_not_installed(script, "dep")
|
||||
|
||||
|
||||
def test_new_resolver_installs_extras(script):
|
||||
create_basic_wheel_for_package(
|
||||
script,
|
||||
"base",
|
||||
"0.1.0",
|
||||
extras={"add": ["dep"]},
|
||||
)
|
||||
create_basic_wheel_for_package(
|
||||
script,
|
||||
"dep",
|
||||
"0.1.0",
|
||||
)
|
||||
result = script.pip(
|
||||
"install", "--unstable-feature=resolver",
|
||||
"--no-cache-dir", "--no-index",
|
||||
"--find-links", script.scratch_path,
|
||||
"base[add,missing]",
|
||||
expect_stderr=True,
|
||||
)
|
||||
assert "WARNING: Invalid extras specified" in result.stderr, str(result)
|
||||
assert ": missing" in result.stderr, str(result)
|
||||
assert_installed(script, base="0.1.0", dep="0.1.0")
|
||||
|
|
Loading…
Reference in New Issue