Merge pull request #7897 from pfmoore/new_resolver_extras

New resolver extras implementation
This commit is contained in:
Paul Moore 2020-03-26 16:29:50 +00:00 committed by GitHub
commit c3d86200b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 29 deletions

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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()
]

View File

@ -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

View File

@ -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")