Repository: pypa/pip

Issue:
Implement yanked message in the new resolver
Following up #8123. I’m planning to tackle this after finishing the wheel cache lookup. The two are related (a wheel cache hit makes it unable to detect a wheel coming from a yanked link), so this needs some more thought than the legacy resolver.


Discussion:

Resolving patch:
diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py
--- a/src/pip/_internal/resolution/resolvelib/base.py
+++ b/src/pip/_internal/resolution/resolvelib/base.py
@@ -7,6 +7,7 @@
 
     from pip._vendor.packaging.version import _BaseVersion
 
+    from pip._internal.models.link import Link
     from pip._internal.req.req_install import InstallRequirement
 
     CandidateLookup = Tuple[
@@ -54,6 +55,11 @@ def is_installed(self):
         # type: () -> bool
         raise NotImplementedError("Override in subclass")
 
+    @property
+    def source_link(self):
+        # type: () -> Optional[Link]
+        raise NotImplementedError("Override in subclass")
+
     def iter_dependencies(self):
         # type: () -> Iterable[Optional[Requirement]]
         raise NotImplementedError("Override in subclass")
diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py
--- a/src/pip/_internal/resolution/resolvelib/candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/candidates.py
@@ -106,19 +106,34 @@ def make_install_req_from_dist(dist, template):
 
 
 class _InstallRequirementBackedCandidate(Candidate):
-    # These are not installed
+    """A candidate backed by an ``InstallRequirement``.
+
+    This represents a package request with the target not being already
+    in the environment, and needs to be fetched and installed. The backing
+    ``InstallRequirement`` is responsible for most of the leg work; this
+    class exposes appropriate information to the resolver.
+
+    :param link: The link passed to the ``InstallRequirement``. The backing
+        ``InstallRequirement`` will use this link to fetch the distribution.
+    :param source_link: The link this candidate "originates" from. This is
+        different from ``link`` when the link is found in the wheel cache.
+        ``link`` would point to the wheel cache, while this points to the
+        found remote link (e.g. from pypi.org).
+    """
     is_installed = False
 
     def __init__(
         self,
         link,          # type: Link
+        source_link,   # type: Link
         ireq,          # type: InstallRequirement
         factory,       # type: Factory
         name=None,     # type: Optional[str]
         version=None,  # type: Optional[_BaseVersion]
     ):
         # type: (...) -> None
-        self.link = link
+        self._link = link
+        self._source_link = source_link
         self._factory = factory
         self._ireq = ireq
         self._name = name
@@ -129,17 +144,17 @@ def __repr__(self):
         # type: () -> str
         return "{class_name}({link!r})".format(
             class_name=self.__class__.__name__,
-            link=str(self.link),
+            link=str(self._link),
         )
 
     def __hash__(self):
         # type: () -> int
-        return hash((self.__class__, self.link))
+        return hash((self.__class__, self._link))
 
     def __eq__(self, other):
         # type: (Any) -> bool
         if isinstance(other, self.__class__):
-            return self.link == other.link
+            return self._link == other._link
         return False
 
     # Needed for Python 2, which does not implement this by default
@@ -147,6 +162,11 @@ def __ne__(self, other):
         # type: (Any) -> bool
         return not self.__eq__(other)
 
+    @property
+    def source_link(self):
+        # type: () -> Optional[Link]
+        return self._source_link
+
     @property
     def name(self):
         # type: () -> str
@@ -234,6 +254,7 @@ def __init__(
         version=None,  # type: Optional[_BaseVersion]
     ):
         # type: (...) -> None
+        source_link = link
         cache_entry = factory.get_wheel_cache_entry(link, name)
         if cache_entry is not None:
             logger.debug("Using cached wheel link: %s", cache_entry.link)
@@ -247,6 +268,7 @@ def __init__(
 
         super(LinkCandidate, self).__init__(
             link=link,
+            source_link=source_link,
             ireq=ireq,
             factory=factory,
             name=name,
@@ -272,6 +294,7 @@ def __init__(
         # type: (...) -> None
         super(EditableCandidate, self).__init__(
             link=link,
+            source_link=link,
             ireq=make_install_req_from_editable(link, template),
             factory=factory,
             name=name,
@@ -285,6 +308,7 @@ def _prepare_abstract_distribution(self):
 
 class AlreadyInstalledCandidate(Candidate):
     is_installed = True
+    source_link = None
 
     def __init__(
         self,
@@ -418,6 +442,11 @@ def is_installed(self):
         # type: () -> _BaseVersion
         return self.base.is_installed
 
+    @property
+    def source_link(self):
+        # type: () -> Optional[Link]
+        return self.base.source_link
+
     def iter_dependencies(self):
         # type: () -> Iterable[Optional[Requirement]]
         factory = self.base._factory
@@ -455,6 +484,7 @@ def get_install_requirement(self):
 
 class RequiresPythonCandidate(Candidate):
     is_installed = False
+    source_link = None
 
     def __init__(self, py_version_info):
         # type: (Optional[Tuple[int, ...]]) -> None
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -167,6 +167,21 @@ def resolve(self, root_reqs, check_supported_wheels):
             ireq = candidate.get_install_requirement()
             if ireq is None:
                 continue
+            link = candidate.source_link
+            if link and link.is_yanked:
+                # The reason can contain non-ASCII characters, Unicode
+                # is required for Python 2.
+                msg = (
+                    u'The candidate selected for download or install is a '
+                    u'yanked version: {name!r} candidate (version {version} '
+                    u'at {link})\nReason for being yanked: {reason}'
+                ).format(
+                    name=candidate.name,
+                    version=candidate.version,
+                    link=link,
+                    reason=link.yanked_reason or u'<none given>',
+                )
+                logger.warning(msg)
             ireq.should_reinstall = self.factory.should_reinstall(candidate)
             req_set.add_named_requirement(ireq)
 


Test patch:
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -1797,7 +1797,6 @@ def test_valid_index_url_argument(script, shared_data):
     assert 'Successfully installed Dinner' in result.stdout, str(result)
 
 
-@pytest.mark.fails_on_new_resolver
 def test_install_yanked_file_and_print_warning(script, data):
     """
     Test install a "yanked" file and print a warning.