diff --git a/news/9232.bugfix.rst b/news/9232.bugfix.rst new file mode 100644 index 00000000000..2d50d1ce41d --- /dev/null +++ b/news/9232.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Make constraints also apply to package variants with extras, so +the resolver correctly avoids backtracking on them. diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index e2edbe9f42c..7eb8a178eb9 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -67,9 +67,25 @@ def is_satisfied_by(self, candidate): class Requirement(object): + @property + def project_name(self): + # type: () -> str + """The "project name" of a requirement. + + This is different from ``name`` if this requirement contains extras, + in which case ``name`` would contain the ``[...]`` part, while this + refers to the name of the project. + """ + raise NotImplementedError("Subclass should override") + @property def name(self): # type: () -> str + """The name identifying this requirement in the resolver. + + This is different from ``project_name`` if this requirement contains + extras, where ``project_name`` would not contain the ``[...]`` part. + """ raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): @@ -86,9 +102,25 @@ def format_for_error(self): class Candidate(object): + @property + def project_name(self): + # type: () -> str + """The "project name" of the candidate. + + This is different from ``name`` if this candidate contains extras, + in which case ``name`` would contain the ``[...]`` part, while this + refers to the name of the project. + """ + raise NotImplementedError("Override in subclass") + @property def name(self): # type: () -> str + """The name identifying this candidate in the resolver. + + This is different from ``project_name`` if this candidate contains + extras, where ``project_name`` would not contain the ``[...]`` part. + """ raise NotImplementedError("Override in subclass") @property diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 1fc2ff479a9..cd1f188706f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -175,13 +175,18 @@ def source_link(self): return self._source_link @property - def name(self): + def project_name(self): # type: () -> str """The normalised name of the project the candidate refers to""" if self._name is None: self._name = canonicalize_name(self.dist.project_name) return self._name + @property + def name(self): + # type: () -> str + return self.project_name + @property def version(self): # type: () -> _BaseVersion @@ -390,10 +395,15 @@ def __ne__(self, other): return not self.__eq__(other) @property - def name(self): + def project_name(self): # type: () -> str return canonicalize_name(self.dist.project_name) + @property + def name(self): + # type: () -> str + return self.project_name + @property def version(self): # type: () -> _BaseVersion @@ -481,11 +491,16 @@ def __ne__(self, other): # type: (Any) -> bool return not self.__eq__(other) + @property + def project_name(self): + # type: () -> str + return self.base.project_name + @property def name(self): # type: () -> str """The normalised name of the project the candidate refers to""" - return format_name(self.base.name, self.extras) + return format_name(self.base.project_name, self.extras) @property def version(self): @@ -572,11 +587,16 @@ def __str__(self): return "Python {}".format(self._version) @property - def name(self): + def project_name(self): # type: () -> str # Avoid conflicting with the PyPI package "Python". return "" + @property + def name(self): + # type: () -> str + return self.project_name + @property def version(self): # type: () -> _BaseVersion diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index c91f252f7b4..3883135f12b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -30,6 +30,16 @@ class PipProvider(AbstractProvider): + """Pip's provider implementation for resolvelib. + + :params constraints: A mapping of constraints specified by the user. Keys + are canonicalized project names. + :params ignore_dependencies: Whether the user specified ``--no-deps``. + :params upgrade_strategy: The user-specified upgrade strategy. + :params user_requested: A set of canonicalized package names that the user + supplied for pip to install/upgrade. + """ + def __init__( self, factory, # type: Factory @@ -113,7 +123,7 @@ def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] if not requirements: return [] - name = requirements[0].name + name = requirements[0].project_name def _eligible_for_upgrade(name): # type: (str) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 25cddceaf62..d926d0a0656 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -28,6 +28,12 @@ def __repr__(self): candidate=self.candidate, ) + @property + def project_name(self): + # type: () -> str + # No need to canonicalise - the candidate did this + return self.candidate.project_name + @property def name(self): # type: () -> str @@ -65,11 +71,15 @@ def __repr__(self): requirement=str(self._ireq.req), ) + @property + def project_name(self): + # type: () -> str + return canonicalize_name(self._ireq.req.name) + @property def name(self): # type: () -> str - canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._extras) + return format_name(self.project_name, self._extras) def format_for_error(self): # type: () -> str @@ -121,6 +131,11 @@ def __repr__(self): specifier=str(self.specifier), ) + @property + def project_name(self): + # type: () -> str + return self._candidate.project_name + @property def name(self): # type: () -> str