From 4fb1496880ea741aea44dbd5c6bc74f773ba9baa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Mon, 13 Jun 2022 17:19:55 +0200
Subject: [PATCH 1/6] repositories/link_sources: support for yanked files
 according to PEP 592

---
 src/poetry/repositories/link_sources/base.py | 14 +++
 src/poetry/repositories/link_sources/html.py |  8 +-
 tests/repositories/link_sources/test_base.py |  6 +-
 tests/repositories/link_sources/test_html.py | 93 ++++++++++++++++++++
 4 files changed, 119 insertions(+), 2 deletions(-)
 create mode 100644 tests/repositories/link_sources/test_html.py

diff --git a/src/poetry/repositories/link_sources/base.py b/src/poetry/repositories/link_sources/base.py
index 824f536c1c3..6e07de3cfdd 100644
--- a/src/poetry/repositories/link_sources/base.py
+++ b/src/poetry/repositories/link_sources/base.py
@@ -113,3 +113,17 @@ def clean_link(self, url: str) -> str:
         the link, it will be rewritten to %20 (while not over-quoting
         % or other characters)."""
         return self.CLEAN_REGEX.sub(lambda match: f"%{ord(match.group(0)):02x}", url)
+
+    def yanked(self, name: NormalizedName, version: Version) -> str | bool:
+        reasons = set()
+        for link in self.links_for_version(name, version):
+            if link.yanked:
+                if link.yanked_reason:
+                    reasons.add(link.yanked_reason)
+            else:
+                # release is not yanked if at least one file is not yanked
+                return False
+        # if all files are yanked (or there are no files) the release is yanked
+        if reasons:
+            return "\n".join(sorted(reasons))
+        return True
diff --git a/src/poetry/repositories/link_sources/html.py b/src/poetry/repositories/link_sources/html.py
index 9f07740642a..d72a8e06ded 100644
--- a/src/poetry/repositories/link_sources/html.py
+++ b/src/poetry/repositories/link_sources/html.py
@@ -33,7 +33,13 @@ def links(self) -> Iterator[Link]:
                 url = self.clean_link(urllib.parse.urljoin(self._url, href))
                 pyrequire = anchor.get("data-requires-python")
                 pyrequire = unescape(pyrequire) if pyrequire else None
-                link = Link(url, requires_python=pyrequire)
+                yanked_value = anchor.get("data-yanked")
+                yanked: str | bool
+                if yanked_value:
+                    yanked = unescape(yanked_value)
+                else:
+                    yanked = "data-yanked" in anchor.attrib
+                link = Link(url, requires_python=pyrequire, yanked=yanked)
 
                 if link.ext not in self.SUPPORTED_FORMATS:
                     continue
diff --git a/tests/repositories/link_sources/test_base.py b/tests/repositories/link_sources/test_base.py
index 2870499854b..c949f90a5eb 100644
--- a/tests/repositories/link_sources/test_base.py
+++ b/tests/repositories/link_sources/test_base.py
@@ -5,6 +5,7 @@
 
 import pytest
 
+from packaging.utils import canonicalize_name
 from poetry.core.packages.package import Package
 from poetry.core.packages.utils.link import Link
 from poetry.core.semver.version import Version
@@ -87,4 +88,7 @@ def test_links_for_version(
 ) -> None:
     version = Version.parse(version_string)
     expected = {Link(f"{link_source.url}/{name}") for name in filenames}
-    assert set(link_source.links_for_version("demo", version)) == expected
+    assert (
+        set(link_source.links_for_version(canonicalize_name("demo"), version))
+        == expected
+    )
diff --git a/tests/repositories/link_sources/test_html.py b/tests/repositories/link_sources/test_html.py
new file mode 100644
index 00000000000..35b9ebfaa88
--- /dev/null
+++ b/tests/repositories/link_sources/test_html.py
@@ -0,0 +1,93 @@
+from __future__ import annotations
+
+import pytest
+
+from poetry.core.packages.utils.link import Link
+from poetry.core.semver.version import Version
+
+from poetry.repositories.link_sources.html import HTMLPage
+
+
+DEMO_TEMPLATE = """
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta name="pypi:repository-version" content="1.0">
+    <title>Links for demo</title>
+  </head>
+  <body>
+    <h1>Links for demo</h1>
+    {}
+    </body>
+</html>
+"""
+
+
+@pytest.mark.parametrize(
+    "attributes, expected_link",
+    [
+        ("", Link("https://example.org/demo-0.1.whl")),
+        (
+            'data-requires-python="&gt;=3.7"',
+            Link("https://example.org/demo-0.1.whl", requires_python=">=3.7"),
+        ),
+        (
+            "data-yanked",
+            Link("https://example.org/demo-0.1.whl", yanked=True),
+        ),
+        (
+            'data-yanked=""',
+            Link("https://example.org/demo-0.1.whl", yanked=True),
+        ),
+        (
+            'data-yanked="&lt;reason&gt;"',
+            Link("https://example.org/demo-0.1.whl", yanked="<reason>"),
+        ),
+        (
+            'data-requires-python="&gt;=3.7" data-yanked',
+            Link(
+                "https://example.org/demo-0.1.whl", requires_python=">=3.7", yanked=True
+            ),
+        ),
+    ],
+)
+def test_link_attributes(attributes: str, expected_link: Link) -> None:
+    anchor = (
+        f'<a href="https://example.org/demo-0.1.whl" {attributes}>demo-0.1.whl</a><br/>'
+    )
+    content = DEMO_TEMPLATE.format(anchor)
+    page = HTMLPage("https://example.org", content)
+
+    assert len(list(page.links)) == 1
+    link = list(page.links)[0]
+    assert link.url == expected_link.url
+    assert link.requires_python == expected_link.requires_python
+    assert link.yanked == expected_link.yanked
+    assert link.yanked_reason == expected_link.yanked_reason
+
+
+@pytest.mark.parametrize(
+    "yanked_attrs, expected",
+    [
+        (("", ""), False),
+        (("data-yanked", ""), False),
+        (("", "data-yanked"), False),
+        (("data-yanked", "data-yanked"), True),
+        (("data-yanked='reason'", "data-yanked"), "reason"),
+        (("data-yanked", "data-yanked='reason'"), "reason"),
+        (("data-yanked='reason'", "data-yanked=''"), "reason"),
+        (("data-yanked=''", "data-yanked='reason'"), "reason"),
+        (("data-yanked='reason'", "data-yanked='reason'"), "reason"),
+        (("data-yanked='reason 1'", "data-yanked='reason 2'"), "reason 1\nreason 2"),
+    ],
+)
+def test_yanked(yanked_attrs: tuple[str, str], expected: bool | str) -> None:
+    anchors = (
+        f'<a href="https://example.org/demo-0.1.tar.gz" {yanked_attrs[0]}>'
+        "demo-0.1.tar.gz</a>"
+        f'<a href="https://example.org/demo-0.1.whl" {yanked_attrs[1]}>demo-0.1.whl</a>'
+    )
+    content = DEMO_TEMPLATE.format(anchors)
+    page = HTMLPage("https://example.org", content)
+
+    assert page.yanked("demo", Version.parse("0.1")) == expected

From feca8219a9dd7a990a5c09a8bd8696a67e915ca4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Mon, 13 Jun 2022 17:37:16 +0200
Subject: [PATCH 2/6] repositories: support for yanked releases according to
 PEP 592

---
 src/poetry/inspection/info.py                 |   6 +
 src/poetry/repositories/cached.py             |   2 +-
 src/poetry/repositories/http.py               |   3 +
 src/poetry/repositories/legacy_repository.py  |  11 +-
 src/poetry/repositories/pypi_repository.py    |  14 +-
 src/poetry/repositories/repository.py         |   7 +-
 tests/console/commands/test_add.py            |   8 +-
 tests/repositories/fixtures/legacy/black.html |   3 +-
 .../fixtures/legacy/futures_partial_yank.html |  12 ++
 .../dists/black-21.11b0-py3-none-any.whl      | Bin 0 -> 2822 bytes
 .../fixtures/pypi.org/json/black.json         |  42 +++++
 .../fixtures/pypi.org/json/black/21.11b0.json | 155 ++++++++++++++++++
 tests/repositories/test_legacy_repository.py  |  74 +++++++++
 tests/repositories/test_pypi_repository.py    |  59 +++++++
 tests/repositories/test_repository.py         |  63 +++++++
 15 files changed, 449 insertions(+), 10 deletions(-)
 create mode 100644 tests/repositories/fixtures/legacy/futures_partial_yank.html
 create mode 100644 tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl
 create mode 100644 tests/repositories/fixtures/pypi.org/json/black/21.11b0.json
 create mode 100644 tests/repositories/test_repository.py

diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py
index 7e851223b5a..1495bc7c62c 100644
--- a/src/poetry/inspection/info.py
+++ b/src/poetry/inspection/info.py
@@ -69,6 +69,7 @@ def __init__(self, path: Path | str, *reasons: BaseException | str) -> None:
 class PackageInfo:
     def __init__(
         self,
+        *,
         name: str | None = None,
         version: str | None = None,
         summary: str | None = None,
@@ -76,6 +77,7 @@ def __init__(
         requires_dist: list[str] | None = None,
         requires_python: str | None = None,
         files: list[dict[str, str]] | None = None,
+        yanked: str | bool = False,
         cache_version: str | None = None,
     ) -> None:
         self.name = name
@@ -85,6 +87,7 @@ def __init__(
         self.requires_dist = requires_dist
         self.requires_python = requires_python
         self.files = files or []
+        self.yanked = yanked
         self._cache_version = cache_version
         self._source_type: str | None = None
         self._source_url: str | None = None
@@ -117,6 +120,7 @@ def asdict(self) -> dict[str, Any]:
             "requires_dist": self.requires_dist,
             "requires_python": self.requires_python,
             "files": self.files,
+            "yanked": self.yanked,
             "_cache_version": self._cache_version,
         }
 
@@ -163,6 +167,7 @@ def to_package(
             source_type=self._source_type,
             source_url=self._source_url,
             source_reference=self._source_reference,
+            yanked=self.yanked,
         )
         if self.summary is not None:
             package.description = self.summary
@@ -450,6 +455,7 @@ def from_package(cls, package: Package) -> PackageInfo:
             requires_dist=list(requires),
             requires_python=package.python_versions,
             files=package.files,
+            yanked=package.yanked_reason if package.yanked else False,
         )
 
     @staticmethod
diff --git a/src/poetry/repositories/cached.py b/src/poetry/repositories/cached.py
index 17f18c37a46..a4b6b6ad089 100644
--- a/src/poetry/repositories/cached.py
+++ b/src/poetry/repositories/cached.py
@@ -21,7 +21,7 @@
 
 
 class CachedRepository(Repository, ABC):
-    CACHE_VERSION = parse_constraint("1.0.0")
+    CACHE_VERSION = parse_constraint("1.1.0")
 
     def __init__(
         self, name: str, disable_cache: bool = False, config: Config | None = None
diff --git a/src/poetry/repositories/http.py b/src/poetry/repositories/http.py
index a3eef5f1643..c63f2b19281 100644
--- a/src/poetry/repositories/http.py
+++ b/src/poetry/repositories/http.py
@@ -210,6 +210,9 @@ def _links_to_data(self, links: list[Link], data: PackageInfo) -> dict[str, Any]
         urls = defaultdict(list)
         files: list[dict[str, Any]] = []
         for link in links:
+            if link.yanked and not data.yanked:
+                # drop yanked files unless the entire release is yanked
+                continue
             if link.is_wheel:
                 urls["bdist_wheel"].append(link.url)
             elif link.filename.endswith(
diff --git a/src/poetry/repositories/legacy_repository.py b/src/poetry/repositories/legacy_repository.py
index 5c115c979f4..51aece929d0 100644
--- a/src/poetry/repositories/legacy_repository.py
+++ b/src/poetry/repositories/legacy_repository.py
@@ -72,7 +72,7 @@ def _find_packages(
         """
         Find packages on the remote server.
         """
-        versions: list[Version]
+        versions: list[tuple[Version, str | bool]]
 
         key: str = name
         if not constraint.is_any():
@@ -90,7 +90,9 @@ def _find_packages(
                 return []
 
             versions = [
-                version for version in page.versions(name) if constraint.allows(version)
+                (version, page.yanked(name, version))
+                for version in page.versions(name)
+                if constraint.allows(version)
             ]
             self._cache.store("matches").put(key, versions, 5)
 
@@ -101,8 +103,9 @@ def _find_packages(
                 source_type="legacy",
                 source_reference=self.name,
                 source_url=self._url,
+                yanked=yanked,
             )
-            for version in versions
+            for version, yanked in versions
         ]
 
     def _get_release_info(
@@ -113,6 +116,7 @@ def _get_release_info(
             raise PackageNotFound(f'No package named "{name}"')
 
         links = list(page.links_for_version(name, version))
+        yanked = page.yanked(name, version)
 
         return self._links_to_data(
             links,
@@ -124,6 +128,7 @@ def _get_release_info(
                 requires_dist=[],
                 requires_python=None,
                 files=[],
+                yanked=yanked,
                 cache_version=str(self.CACHE_VERSION),
             ),
         )
diff --git a/src/poetry/repositories/pypi_repository.py b/src/poetry/repositories/pypi_repository.py
index 3413aedde45..277df3cf1ae 100644
--- a/src/poetry/repositories/pypi_repository.py
+++ b/src/poetry/repositories/pypi_repository.py
@@ -144,7 +144,10 @@ def _find_packages(
                 continue
 
             if constraint.allows(version):
-                packages.append(Package(info["info"]["name"], version))
+                # PEP 592: PyPI always yanks entire releases, not individual files,
+                # so we just have to look for the first file
+                yanked = self._get_yanked(release[0])
+                packages.append(Package(info["info"]["name"], version, yanked=yanked))
 
         return packages
 
@@ -163,7 +166,7 @@ def find_links_for_package(self, package: Package) -> list[Link]:
         links = []
         for url in json_data["urls"]:
             h = f"sha256={url['digests']['sha256']}"
-            links.append(Link(url["url"] + "#" + h))
+            links.append(Link(url["url"] + "#" + h, yanked=self._get_yanked(url)))
 
         return links
 
@@ -188,6 +191,7 @@ def _get_release_info(
             requires_dist=info["requires_dist"],
             requires_python=info["requires_python"],
             files=info.get("files", []),
+            yanked=self._get_yanked(info),
             cache_version=str(self.CACHE_VERSION),
         )
 
@@ -254,3 +258,9 @@ def _get(self, endpoint: str) -> dict[str, Any] | None:
 
         json: dict[str, Any] = json_response.json()
         return json
+
+    @staticmethod
+    def _get_yanked(json_data: dict[str, Any]) -> str | bool:
+        if json_data.get("yanked", False):
+            return json_data.get("yanked_reason") or True  # noqa: SIM222
+        return False
diff --git a/src/poetry/repositories/repository.py b/src/poetry/repositories/repository.py
index b5933a23d66..2d823bf7276 100644
--- a/src/poetry/repositories/repository.py
+++ b/src/poetry/repositories/repository.py
@@ -5,6 +5,7 @@
 from typing import TYPE_CHECKING
 
 from poetry.core.semver.helpers import parse_constraint
+from poetry.core.semver.version import Version
 from poetry.core.semver.version_constraint import VersionConstraint
 from poetry.core.semver.version_range import VersionRange
 
@@ -16,7 +17,6 @@
     from poetry.core.packages.dependency import Dependency
     from poetry.core.packages.package import Package
     from poetry.core.packages.utils.link import Link
-    from poetry.core.semver.version import Version
 
 
 class Repository:
@@ -43,6 +43,11 @@ def find_packages(self, dependency: Dependency) -> list[Package]:
         ignored_pre_release_packages = []
 
         for package in self._find_packages(dependency.name, constraint):
+            if package.yanked and not isinstance(constraint, Version):
+                # PEP 592: yanked files are always ignored, unless they are the only
+                # file that matches a version specifier that "pins" to an exact
+                # version
+                continue
             if (
                 package.is_prerelease()
                 and not allow_prereleases
diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py
index 27f803d3915..72dc8836825 100644
--- a/tests/console/commands/test_add.py
+++ b/tests/console/commands/test_add.py
@@ -823,7 +823,9 @@ def test_add_constraint_with_source(
 ):
     repo = LegacyRepository(name="my-index", url="https://my-index.fake")
     repo.add_package(get_package("cachy", "0.2.0"))
-    repo._cache.store("matches").put("cachy:0.2.0", [Version.parse("0.2.0")], 5)
+    repo._cache.store("matches").put(
+        "cachy:0.2.0", [(Version.parse("0.2.0"), False)], 5
+    )
 
     poetry.pool.add_repository(repo)
 
@@ -1810,7 +1812,9 @@ def test_add_constraint_with_source_old_installer(
 ):
     repo = LegacyRepository(name="my-index", url="https://my-index.fake")
     repo.add_package(get_package("cachy", "0.2.0"))
-    repo._cache.store("matches").put("cachy:0.2.0", [Version.parse("0.2.0")], 5)
+    repo._cache.store("matches").put(
+        "cachy:0.2.0", [(Version.parse("0.2.0"), False)], 5
+    )
 
     poetry.pool.add_repository(repo)
 
diff --git a/tests/repositories/fixtures/legacy/black.html b/tests/repositories/fixtures/legacy/black.html
index 9d3fa08c633..ea050662b83 100644
--- a/tests/repositories/fixtures/legacy/black.html
+++ b/tests/repositories/fixtures/legacy/black.html
@@ -4,7 +4,8 @@
     <title>Links for black</title></head>
 <body>
     <h1>Links for black</h1>
-    <a href="https://files.pythonhosted.org/packages/b0/dc/ecd83b973fb7b82c34d828aad621a6e5865764d52375b8ac1d7a45e23c8d/black-19.10b0.tar.gz#sha256=c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539" data-requires-python=">=3.6">black-19.10b0.tar.gz</a>
+    <a href="https://files.pythonhosted.org/packages/fd/bb/ad34bbc93d1bea3de086d7c59e528d4a503ac8fe318bd1fa48605584c3d2/black-19.10b0-py36-none-any.whl#sha256=1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b" data-requires-python=">=3.6">black-19.10b0-py36-none-any.whl</a>
+    <a href="https://files.pythonhosted.org/packages/3d/ad/1cf514e7f9ee4c3d8df7c839d7977f7605ad76557f3fca741ec67f76dba6/black-21.11b0-py3-none-any.whl#sha256=0b1f66cbfadcd332ceeaeecf6373d9991d451868d2e2219ad0ac1213fb701117" data-requires-python=">=3.6.2" data-yanked="Broken regex dependency. Use 21.11b1 instead.">black-21.11b0-py3-none-any.whl</a>
     </body>
 </html>
 <!--SERIAL 6044498-->
diff --git a/tests/repositories/fixtures/legacy/futures_partial_yank.html b/tests/repositories/fixtures/legacy/futures_partial_yank.html
new file mode 100644
index 00000000000..41fa4ac3d38
--- /dev/null
+++ b/tests/repositories/fixtures/legacy/futures_partial_yank.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Links for futures</title>
+  </head>
+  <body>
+    <h1>Links for futures</h1>
+    <a href="https://files.pythonhosted.org/packages/2d/99/b2c4e9d5a30f6471e410a146232b4118e697fa3ffc06d6a65efde84debd0/futures-3.2.0-py2-none-any.whl#sha256=ec0a6cb848cc212002b9828c3e34c675e0c9ff6741dc445cab6fdd4e1085d1f1" data-requires-python="&gt;=2.6, &lt;3" data-yanked>futures-3.2.0-py2-none-any.whl</a><br/>
+    <a href="https://files.pythonhosted.org/packages/1f/9e/7b2ff7e965fc654592269f2906ade1c7d705f1bf25b7d469fa153f7d19eb/futures-3.2.0.tar.gz#sha256=9ec02aa7d674acb8618afb127e27fde7fc68994c0437ad759fa094a574adb265" data-requires-python="&gt;=2.6, &lt;3">futures-3.2.0.tar.gz</a><br/>
+    </body>
+</html>
+<!--SERIAL 3865286-->
diff --git a/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl b/tests/repositories/fixtures/pypi.org/dists/black-21.11b0-py3-none-any.whl
new file mode 100644
index 0000000000000000000000000000000000000000..f0e3956e20f5855ef5b7c52a9e93fb38c60cc930
GIT binary patch
literal 2822
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

literal 0
HcmV?d00001

diff --git a/tests/repositories/fixtures/pypi.org/json/black.json b/tests/repositories/fixtures/pypi.org/json/black.json
index 17c61d29de4..38db6d8c89c 100644
--- a/tests/repositories/fixtures/pypi.org/json/black.json
+++ b/tests/repositories/fixtures/pypi.org/json/black.json
@@ -99,6 +99,48 @@
         "yanked": false,
         "yanked_reason": null
       }
+    ],
+    "21.11b0": [
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "945da11b34c11738560fc6698cffa425",
+          "sha256": "0b1f66cbfadcd332ceeaeecf6373d9991d451868d2e2219ad0ac1213fb701117"
+        },
+        "downloads": -1,
+        "filename": "black-21.11b0-py3-none-any.whl",
+        "has_sig": false,
+        "md5_digest": "945da11b34c11738560fc6698cffa425",
+        "packagetype": "bdist_wheel",
+        "python_version": "py3",
+        "requires_python": ">=3.6.2",
+        "size": 155131,
+        "upload_time": "2021-11-17T02:32:14",
+        "upload_time_iso_8601": "2021-11-17T02:32:14.551680Z",
+        "url": "https://files.pythonhosted.org/packages/3d/ad/1cf514e7f9ee4c3d8df7c839d7977f7605ad76557f3fca741ec67f76dba6/black-21.11b0-py3-none-any.whl",
+        "yanked": true,
+        "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+      },
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "6040b4e4c6ccc4e7eb81bb2634ef299a",
+          "sha256": "83f3852301c8dcb229e9c444dd79f573c8d31c7c2dad9bbaaa94c808630e32aa"
+        },
+        "downloads": -1,
+        "filename": "black-21.11b0.tar.gz",
+        "has_sig": false,
+        "md5_digest": "6040b4e4c6ccc4e7eb81bb2634ef299a",
+        "packagetype": "sdist",
+        "python_version": "source",
+        "requires_python": ">=3.6.2",
+        "size": 593164,
+        "upload_time": "2021-11-17T02:32:16",
+        "upload_time_iso_8601": "2021-11-17T02:32:16.396821Z",
+        "url": "https://files.pythonhosted.org/packages/2f/db/03e8cef689ab0ff857576ee2ee288d1ff2110ef7f3a77cac62e61f18acaf/black-21.11b0.tar.gz",
+        "yanked": true,
+        "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+      }
     ]
   },
   "urls": [
diff --git a/tests/repositories/fixtures/pypi.org/json/black/21.11b0.json b/tests/repositories/fixtures/pypi.org/json/black/21.11b0.json
new file mode 100644
index 00000000000..f816ef3b96b
--- /dev/null
+++ b/tests/repositories/fixtures/pypi.org/json/black/21.11b0.json
@@ -0,0 +1,155 @@
+{
+  "info": {
+    "author": "Ɓukasz Langa",
+    "author_email": "lukasz@langa.pl",
+    "bugtrack_url": null,
+    "classifiers": [
+      "Development Status :: 4 - Beta",
+      "Environment :: Console",
+      "Intended Audience :: Developers",
+      "License :: OSI Approved :: MIT License",
+      "Operating System :: OS Independent",
+      "Programming Language :: Python",
+      "Programming Language :: Python :: 3.6",
+      "Programming Language :: Python :: 3.7",
+      "Programming Language :: Python :: 3.8",
+      "Programming Language :: Python :: 3 :: Only",
+      "Topic :: Software Development :: Libraries :: Python Modules",
+      "Topic :: Software Development :: Quality Assurance"
+    ],
+    "description": "",
+    "description_content_type": "text/markdown",
+    "docs_url": null,
+    "download_url": "",
+    "downloads": {
+      "last_day": -1,
+      "last_month": -1,
+      "last_week": -1
+    },
+    "home_page": "https://github.com/psf/black",
+    "keywords": "automation formatter yapf autopep8 pyfmt gofmt rustfmt",
+    "license": "MIT",
+    "maintainer": "",
+    "maintainer_email": "",
+    "name": "black",
+    "package_url": "https://pypi.org/project/black/",
+    "platform": "",
+    "project_url": "https://pypi.org/project/black/",
+    "project_urls": {
+      "Changelog": "https://github.com/psf/black/blob/main/CHANGES.md",
+      "Homepage": "https://github.com/psf/black"
+    },
+    "release_url": "https://pypi.org/project/black/21.11b0/",
+    "requires_dist": [
+      "click (>=7.1.2)",
+      "platformdirs (>=2)",
+      "tomli (<2.0.0,>=0.2.6)",
+      "regex (>=2020.1.8)",
+      "pathspec (<1,>=0.9.0)",
+      "typing-extensions (>=3.10.0.0)",
+      "mypy-extensions (>=0.4.3)",
+      "dataclasses (>=0.6) ; python_version < \"3.7\"",
+      "typed-ast (>=1.4.2) ; python_version < \"3.8\" and implementation_name == \"cpython\"",
+      "typing-extensions (!=3.10.0.1) ; python_version >= \"3.10\"",
+      "colorama (>=0.4.3) ; extra == 'colorama'",
+      "aiohttp (>=3.7.4) ; extra == 'd'",
+      "ipython (>=7.8.0) ; extra == 'jupyter'",
+      "tokenize-rt (>=3.2.0) ; extra == 'jupyter'",
+      "typed-ast (>=1.4.3) ; extra == 'python2'",
+      "uvloop (>=0.15.2) ; extra == 'uvloop'"
+    ],
+    "requires_python": ">=3.6.2",
+    "summary": "The uncompromising code formatter.",
+    "version": "21.11b0",
+    "yanked": true,
+    "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+  },
+  "last_serial": 13326107,
+  "releases": {
+    "21.11b0": [
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "945da11b34c11738560fc6698cffa425",
+          "sha256": "0b1f66cbfadcd332ceeaeecf6373d9991d451868d2e2219ad0ac1213fb701117"
+        },
+        "downloads": -1,
+        "filename": "black-21.11b0-py3-none-any.whl",
+        "has_sig": false,
+        "md5_digest": "945da11b34c11738560fc6698cffa425",
+        "packagetype": "bdist_wheel",
+        "python_version": "py3",
+        "requires_python": ">=3.6.2",
+        "size": 155131,
+        "upload_time": "2021-11-17T02:32:14",
+        "upload_time_iso_8601": "2021-11-17T02:32:14.551680Z",
+        "url": "https://files.pythonhosted.org/packages/3d/ad/1cf514e7f9ee4c3d8df7c839d7977f7605ad76557f3fca741ec67f76dba6/black-21.11b0-py3-none-any.whl",
+        "yanked": true,
+        "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+      },
+      {
+        "comment_text": "",
+        "digests": {
+          "md5": "6040b4e4c6ccc4e7eb81bb2634ef299a",
+          "sha256": "83f3852301c8dcb229e9c444dd79f573c8d31c7c2dad9bbaaa94c808630e32aa"
+        },
+        "downloads": -1,
+        "filename": "black-21.11b0.tar.gz",
+        "has_sig": false,
+        "md5_digest": "6040b4e4c6ccc4e7eb81bb2634ef299a",
+        "packagetype": "sdist",
+        "python_version": "source",
+        "requires_python": ">=3.6.2",
+        "size": 593164,
+        "upload_time": "2021-11-17T02:32:16",
+        "upload_time_iso_8601": "2021-11-17T02:32:16.396821Z",
+        "url": "https://files.pythonhosted.org/packages/2f/db/03e8cef689ab0ff857576ee2ee288d1ff2110ef7f3a77cac62e61f18acaf/black-21.11b0.tar.gz",
+        "yanked": true,
+        "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+      }
+    ]
+  },
+  "urls": [
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "945da11b34c11738560fc6698cffa425",
+        "sha256": "0b1f66cbfadcd332ceeaeecf6373d9991d451868d2e2219ad0ac1213fb701117"
+      },
+      "downloads": -1,
+      "filename": "black-21.11b0-py3-none-any.whl",
+      "has_sig": false,
+      "md5_digest": "945da11b34c11738560fc6698cffa425",
+      "packagetype": "bdist_wheel",
+      "python_version": "py3",
+      "requires_python": ">=3.6.2",
+      "size": 155131,
+      "upload_time": "2021-11-17T02:32:14",
+      "upload_time_iso_8601": "2021-11-17T02:32:14.551680Z",
+      "url": "https://files.pythonhosted.org/packages/3d/ad/1cf514e7f9ee4c3d8df7c839d7977f7605ad76557f3fca741ec67f76dba6/black-21.11b0-py3-none-any.whl",
+      "yanked": true,
+      "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+    },
+    {
+      "comment_text": "",
+      "digests": {
+        "md5": "6040b4e4c6ccc4e7eb81bb2634ef299a",
+        "sha256": "83f3852301c8dcb229e9c444dd79f573c8d31c7c2dad9bbaaa94c808630e32aa"
+      },
+      "downloads": -1,
+      "filename": "black-21.11b0.tar.gz",
+      "has_sig": false,
+      "md5_digest": "6040b4e4c6ccc4e7eb81bb2634ef299a",
+      "packagetype": "sdist",
+      "python_version": "source",
+      "requires_python": ">=3.6.2",
+      "size": 593164,
+      "upload_time": "2021-11-17T02:32:16",
+      "upload_time_iso_8601": "2021-11-17T02:32:16.396821Z",
+      "url": "https://files.pythonhosted.org/packages/2f/db/03e8cef689ab0ff857576ee2ee288d1ff2110ef7f3a77cac62e61f18acaf/black-21.11b0.tar.gz",
+      "yanked": true,
+      "yanked_reason": "Broken regex dependency. Use 21.11b1 instead."
+    }
+  ],
+  "vulnerabilities": []
+}
diff --git a/tests/repositories/test_legacy_repository.py b/tests/repositories/test_legacy_repository.py
index a02cff0b127..be1d646b50d 100644
--- a/tests/repositories/test_legacy_repository.py
+++ b/tests/repositories/test_legacy_repository.py
@@ -234,6 +234,24 @@ def test_find_packages_only_prereleases_empty_when_not_any() -> None:
     assert len(packages) == 0
 
 
+@pytest.mark.parametrize(
+    ["constraint", "expected"],
+    [
+        # yanked 21.11b0 is ignored except for pinned version
+        ("*", ["19.10b0"]),
+        (">=19.0a0", ["19.10b0"]),
+        (">=20.0a0", []),
+        (">=21.11b0", []),
+        ("==21.11b0", ["21.11b0"]),
+    ],
+)
+def test_find_packages_yanked(constraint: str, expected: list[str]) -> None:
+    repo = MockRepository()
+    packages = repo.find_packages(Factory.create_dependency("black", constraint))
+
+    assert [str(p.version) for p in packages] == expected
+
+
 def test_get_package_information_chooses_correct_distribution() -> None:
     repo = MockRepository()
 
@@ -402,6 +420,62 @@ def test_get_package_retrieves_packages_with_no_hashes() -> None:
     ] == package.files
 
 
+@pytest.mark.parametrize(
+    "package_name, version, yanked, yanked_reason",
+    [
+        ("black", "19.10b0", False, ""),
+        ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."),
+    ],
+)
+def test_package_yanked(
+    package_name: str, version: str, yanked: bool, yanked_reason: str
+) -> None:
+    repo = MockRepository()
+
+    package = repo.package(canonicalize_name(package_name), Version.parse(version))
+
+    assert package.name == package_name
+    assert str(package.version) == version
+    assert package.yanked is yanked
+    assert package.yanked_reason == yanked_reason
+
+
+def test_package_partial_yank():
+    class SpecialMockRepository(MockRepository):
+        def _get_page(self, endpoint: str) -> SimpleRepositoryPage | None:
+            return super()._get_page(f"/{endpoint.strip('/')}_partial_yank/")
+
+    repo = MockRepository()
+    package = repo.package(canonicalize_name("futures"), Version.parse("3.2.0"))
+    assert len(package.files) == 2
+
+    repo = SpecialMockRepository()
+    package = repo.package(canonicalize_name("futures"), Version.parse("3.2.0"))
+    assert len(package.files) == 1
+    assert package.files[0]["file"].endswith(".tar.gz")
+
+
+@pytest.mark.parametrize(
+    "package_name, version, yanked, yanked_reason",
+    [
+        ("black", "19.10b0", False, ""),
+        ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."),
+    ],
+)
+def test_find_links_for_package_yanked(
+    package_name: str, version: str, yanked: bool, yanked_reason: str
+) -> None:
+    repo = MockRepository()
+
+    package = repo.package(canonicalize_name(package_name), Version.parse(version))
+    links = repo.find_links_for_package(package)
+
+    assert len(links) == 1
+    for link in links:
+        assert link.yanked == yanked
+        assert link.yanked_reason == yanked_reason
+
+
 class MockHttpRepository(LegacyRepository):
     def __init__(
         self, endpoint_responses: dict, http: type[httpretty.httpretty]
diff --git a/tests/repositories/test_pypi_repository.py b/tests/repositories/test_pypi_repository.py
index 9f7371f34c7..2e398de7272 100644
--- a/tests/repositories/test_pypi_repository.py
+++ b/tests/repositories/test_pypi_repository.py
@@ -96,6 +96,24 @@ def test_find_packages_only_prereleases(constraint: str, count: int) -> None:
     assert len(packages) == count
 
 
+@pytest.mark.parametrize(
+    ["constraint", "expected"],
+    [
+        # yanked 21.11b0 is ignored except for pinned version
+        ("*", ["19.10b0"]),
+        (">=19.0a0", ["19.10b0"]),
+        (">=20.0a0", []),
+        (">=21.11b0", []),
+        ("==21.11b0", ["21.11b0"]),
+    ],
+)
+def test_find_packages_yanked(constraint: str, expected: list[str]) -> None:
+    repo = MockRepository()
+    packages = repo.find_packages(Factory.create_dependency("black", constraint))
+
+    assert [str(p.version) for p in packages] == expected
+
+
 def test_package() -> None:
     repo = MockRepository()
 
@@ -128,6 +146,47 @@ def test_package() -> None:
     )
 
 
+@pytest.mark.parametrize(
+    "package_name, version, yanked, yanked_reason",
+    [
+        ("black", "19.10b0", False, ""),
+        ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."),
+    ],
+)
+def test_package_yanked(
+    package_name: str, version: str, yanked: bool, yanked_reason: str
+) -> None:
+    repo = MockRepository()
+
+    package = repo.package(package_name, version)
+
+    assert package.name == package_name
+    assert str(package.version) == version
+    assert package.yanked is yanked
+    assert package.yanked_reason == yanked_reason
+
+
+@pytest.mark.parametrize(
+    "package_name, version, yanked, yanked_reason",
+    [
+        ("black", "19.10b0", False, ""),
+        ("black", "21.11b0", True, "Broken regex dependency. Use 21.11b1 instead."),
+    ],
+)
+def test_find_links_for_package_yanked(
+    package_name: str, version: str, yanked: bool, yanked_reason: str
+) -> None:
+    repo = MockRepository()
+
+    package = repo.package(package_name, version)
+    links = repo.find_links_for_package(package)
+
+    assert len(links) == 2
+    for link in links:
+        assert link.yanked == yanked
+        assert link.yanked_reason == yanked_reason
+
+
 def test_fallback_on_downloading_packages() -> None:
     repo = MockRepository(fallback=True)
 
diff --git a/tests/repositories/test_repository.py b/tests/repositories/test_repository.py
new file mode 100644
index 00000000000..4073d92e53a
--- /dev/null
+++ b/tests/repositories/test_repository.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import pytest
+
+from packaging.utils import canonicalize_name
+from poetry.core.packages.package import Package
+from poetry.core.semver.version import Version
+
+from poetry.factory import Factory
+from poetry.repositories import Repository
+
+
+@pytest.fixture(scope="module")
+def black_repository() -> Repository:
+    repo = Repository("repo")
+    repo.add_package(Package("black", "19.10b0"))
+    repo.add_package(Package("black", "21.11b0", yanked="reason"))
+    return repo
+
+
+@pytest.mark.parametrize(
+    ["constraint", "expected"],
+    [
+        # yanked 21.11b0 is ignored except for pinned version
+        ("*", ["19.10b0"]),
+        (">=19.0a0", ["19.10b0"]),
+        (">=20.0a0", []),
+        (">=21.11b0", []),
+        ("==21.11b0", ["21.11b0"]),
+    ],
+)
+def test_find_packages_yanked(
+    black_repository: Repository, constraint: str, expected: list[str]
+) -> None:
+    packages = black_repository.find_packages(
+        Factory.create_dependency("black", constraint)
+    )
+
+    assert [str(p.version) for p in packages] == expected
+
+
+@pytest.mark.parametrize(
+    "package_name, version, yanked, yanked_reason",
+    [
+        ("black", "19.10b0", False, ""),
+        ("black", "21.11b0", True, "reason"),
+    ],
+)
+def test_package_yanked(
+    black_repository: Repository,
+    package_name: str,
+    version: str,
+    yanked: bool,
+    yanked_reason: str,
+) -> None:
+    package = black_repository.package(
+        canonicalize_name(package_name), Version.parse(version)
+    )
+
+    assert package.name == package_name
+    assert str(package.version) == version
+    assert package.yanked is yanked
+    assert package.yanked_reason == yanked_reason

From 4e0a658c36fb35daa9dbc9fca5609d6c9543d801 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Mon, 13 Jun 2022 17:41:25 +0200
Subject: [PATCH 3/6] solver: support for yanked releases according to PEP 592

---
 src/poetry/puzzle/provider.py                 |  1 +
 tests/helpers.py                              |  6 ++-
 tests/mixology/helpers.py                     | 14 +++---
 .../version_solver/test_basic_graph.py        | 43 +++++++++++++++++++
 .../mixology/version_solver/test_with_lock.py | 28 ++++++++++++
 5 files changed, 85 insertions(+), 7 deletions(-)

diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py
index fa6bedd98e5..b1fd6d10e19 100644
--- a/src/poetry/puzzle/provider.py
+++ b/src/poetry/puzzle/provider.py
@@ -290,6 +290,7 @@ def search_for(self, dependency: Dependency) -> list[DependencyPackage]:
 
         packages.sort(
             key=lambda p: (
+                not p.yanked,
                 not p.is_prerelease() and not dependency.allows_prereleases(),
                 p.version,
             ),
diff --git a/tests/helpers.py b/tests/helpers.py
index b9cd9af5392..05c0ea7bdd9 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -45,8 +45,10 @@
 MOCK_DEFAULT_GIT_REVISION = "9cf87a285a2d3fbb0b9fa621997b3acc3631ed24"
 
 
-def get_package(name: str, version: str | Version) -> Package:
-    return Package(name, version)
+def get_package(
+    name: str, version: str | Version, yanked: str | bool = False
+) -> Package:
+    return Package(name, version, yanked=yanked)
 
 
 def get_dependency(
diff --git a/tests/mixology/helpers.py b/tests/mixology/helpers.py
index 0120bdb72ff..225807e2208 100644
--- a/tests/mixology/helpers.py
+++ b/tests/mixology/helpers.py
@@ -13,6 +13,7 @@
 if TYPE_CHECKING:
     from poetry.core.packages.project_package import ProjectPackage
 
+    from poetry.mixology import SolverResult
     from poetry.repositories import Repository
     from tests.mixology.version_solver.conftest import Provider
 
@@ -23,8 +24,9 @@ def add_to_repo(
     version: str,
     deps: dict[str, str] | None = None,
     python: str | None = None,
+    yanked: bool = False,
 ) -> None:
-    package = Package(name, version)
+    package = Package(name, version, yanked=yanked)
     if python:
         package.python_versions = python
 
@@ -43,7 +45,7 @@ def check_solver_result(
     tries: int | None = None,
     locked: dict[str, Package] | None = None,
     use_latest: list[str] | None = None,
-) -> None:
+) -> SolverResult | None:
     if locked is not None:
         locked = {
             k: [DependencyPackage(l.to_dependency(), l)] for k, l in locked.items()
@@ -59,20 +61,22 @@ def check_solver_result(
             if tries is not None:
                 assert solver.solution.attempted_solutions == tries
 
-            return
+            return None
 
         raise
     except AssertionError as e:
         if error:
             assert str(e) == error
-            return
+            return None
         raise
 
     packages = {}
     for package in solution.packages:
         packages[package.name] = str(package.version)
 
-    assert result == packages
+    assert packages == result
 
     if tries is not None:
         assert solution.attempted_solutions == tries
+
+    return solution
diff --git a/tests/mixology/version_solver/test_basic_graph.py b/tests/mixology/version_solver/test_basic_graph.py
index f8758f00c3e..210abc371e4 100644
--- a/tests/mixology/version_solver/test_basic_graph.py
+++ b/tests/mixology/version_solver/test_basic_graph.py
@@ -2,6 +2,8 @@
 
 from typing import TYPE_CHECKING
 
+import pytest
+
 from poetry.factory import Factory
 from tests.mixology.helpers import add_to_repo
 from tests.mixology.helpers import check_solver_result
@@ -87,3 +89,44 @@ def test_circular_dependency(
     add_to_repo(repo, "bar", "1.0.0", deps={"foo": "1.0.0"})
 
     check_solver_result(root, provider, {"foo": "1.0.0", "bar": "1.0.0"})
+
+
+@pytest.mark.parametrize(
+    "constraint, versions, yanked_versions, expected",
+    [
+        (">=1", ["1", "2"], [], "2"),
+        (">=1", ["1", "2"], ["2"], "1"),
+        (">=1", ["1", "2", "3"], ["2"], "3"),
+        (">=1", ["1", "2", "3"], ["2", "3"], "1"),
+        (">1", ["1", "2"], ["2"], "error"),
+        (">1", ["2"], ["2"], "error"),
+        (">=2", ["2"], ["2"], "error"),
+        ("==2", ["2"], ["2"], "2"),
+        ("==2", ["2", "2+local"], [], "2+local"),
+        ("==2", ["2", "2+local"], ["2+local"], "2"),
+    ],
+)
+def test_yanked_release(
+    root: ProjectPackage,
+    provider: Provider,
+    repo: Repository,
+    constraint: str,
+    versions: list[str],
+    yanked_versions: list[str],
+    expected: str,
+) -> None:
+    root.add_dependency(Factory.create_dependency("foo", constraint))
+
+    for version in versions:
+        add_to_repo(repo, "foo", version, yanked=version in yanked_versions)
+
+    if expected == "error":
+        result = None
+        error = (
+            f"Because myapp depends on foo ({constraint}) which doesn't match any "
+            "versions, version solving failed."
+        )
+    else:
+        result = {"foo": expected}
+        error = None
+    check_solver_result(root, provider, result, error)
diff --git a/tests/mixology/version_solver/test_with_lock.py b/tests/mixology/version_solver/test_with_lock.py
index e9d07d21d7b..fa0e536c3ba 100644
--- a/tests/mixology/version_solver/test_with_lock.py
+++ b/tests/mixology/version_solver/test_with_lock.py
@@ -169,3 +169,31 @@ def test_with_compatible_locked_dependencies_with_extras(
             "baz": get_package("baz", "1.0.0"),
         },
     )
+
+
+def test_with_yanked_package_in_lock(
+    root: ProjectPackage, provider: Provider, repo: Repository
+):
+    root.add_dependency(Factory.create_dependency("foo", "*"))
+
+    add_to_repo(repo, "foo", "1")
+    add_to_repo(repo, "foo", "2", yanked=True)
+
+    # yanked version is kept in lock file
+    locked_foo = get_package("foo", "2")
+    assert not locked_foo.yanked
+    result = check_solver_result(
+        root,
+        provider,
+        result={"foo": "2"},
+        locked={"foo": locked_foo},
+    )
+    foo = result.packages[0]
+    assert foo.yanked
+
+    # without considering the lock file, the other version is chosen
+    check_solver_result(
+        root,
+        provider,
+        result={"foo": "1"},
+    )

From 0d623b9a9a90f48083f8318121edcebcde577849 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Mon, 13 Jun 2022 17:42:53 +0200
Subject: [PATCH 4/6] solver: print warning if yanked release is chosen

---
 src/poetry/puzzle/solver.py | 10 +++++++++
 tests/puzzle/test_solver.py | 42 +++++++++++++++++++++++++++++++++++++
 2 files changed, 52 insertions(+)

diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py
index 815e5d54766..fb3c389f979 100644
--- a/src/poetry/puzzle/solver.py
+++ b/src/poetry/puzzle/solver.py
@@ -83,6 +83,16 @@ def solve(self, use_latest: list[str] | None = None) -> Transaction:
                     f" {', '.join(f'({b})' for b in self._overrides)}"
                 )
 
+        for p in packages:
+            if p.yanked:
+                message = (
+                    f"The locked version {p.pretty_version} for {p.pretty_name} is a"
+                    " yanked version."
+                )
+                if p.yanked_reason:
+                    message += f" Reason for being yanked: {p.yanked_reason}"
+                self._io.write_error_line(f"<warning>Warning: {message}</warning>")
+
         return Transaction(
             self._locked_packages,
             list(zip(packages, depths)),
diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py
index 7af0fcaf540..12a8fdb2062 100644
--- a/tests/puzzle/test_solver.py
+++ b/tests/puzzle/test_solver.py
@@ -6,6 +6,7 @@
 
 import pytest
 
+from cleo.io.buffered_io import BufferedIO
 from cleo.io.null_io import NullIO
 from poetry.core.packages.dependency import Dependency
 from poetry.core.packages.package import Package
@@ -3680,3 +3681,44 @@ def test_update_with_prerelease_and_no_solution(
 
     with pytest.raises(SolverProblemError):
         solver.solve()
+
+
+def test_solver_yanked_warning(
+    package: ProjectPackage,
+    installed: InstalledRepository,
+    locked: Repository,
+    pool: Pool,
+    repo: Repository,
+) -> None:
+    package.add_dependency(Factory.create_dependency("foo", "==1"))
+    package.add_dependency(Factory.create_dependency("bar", "==2"))
+    package.add_dependency(Factory.create_dependency("baz", "==3"))
+    foo = get_package("foo", "1", yanked=False)
+    bar = get_package("bar", "2", yanked=True)
+    baz = get_package("baz", "3", yanked="just wrong")
+    repo.add_package(foo)
+    repo.add_package(bar)
+    repo.add_package(baz)
+
+    io = BufferedIO(decorated=False)
+    solver = Solver(package, pool, installed.packages, locked.packages, io)
+    transaction = solver.solve()
+
+    check_solver_result(
+        transaction,
+        [
+            {"job": "install", "package": bar},
+            {"job": "install", "package": baz},
+            {"job": "install", "package": foo},
+        ],
+    )
+    error = io.fetch_error()
+    assert "foo" not in error
+    assert "The locked version 2 for bar is a yanked version." in error
+    assert (
+        "The locked version 3 for baz is a yanked version. Reason for being yanked:"
+        " just wrong"
+        in error
+    )
+    assert error.count("is a yanked version") == 2
+    assert error.count("Reason for being yanked") == 1

From db24665389a28499aedc2b1ef7c9c0bcda8c5bf7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Mon, 13 Jun 2022 17:44:19 +0200
Subject: [PATCH 5/6] chooser: prefer files that are not yanked

---
 src/poetry/installation/chooser.py |   3 +-
 tests/installation/test_chooser.py | 100 +++++++++++++++++++++++++++++
 2 files changed, 101 insertions(+), 2 deletions(-)

diff --git a/src/poetry/installation/chooser.py b/src/poetry/installation/chooser.py
index 600a46306ce..53e746fdfd2 100644
--- a/src/poetry/installation/chooser.py
+++ b/src/poetry/installation/chooser.py
@@ -198,8 +198,7 @@ def _sort_key(
 
         has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package))
 
-        # TODO: Proper yank value
-        yank_value = 0
+        yank_value = int(not link.yanked)
 
         return (
             has_allowed_hash,
diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py
index 51c8b66c2ab..b4a8d36bc26 100644
--- a/tests/installation/test_chooser.py
+++ b/tests/installation/test_chooser.py
@@ -90,6 +90,26 @@ def callback(
     )
 
 
+@pytest.fixture()
+def mock_legacy_partial_yank(http: type[httpretty.httpretty]) -> None:
+    def callback(
+        request: HTTPrettyRequest, uri: str, headers: dict[str, Any]
+    ) -> list[int | dict[str, Any] | str]:
+        parts = uri.rsplit("/")
+        name = parts[-2]
+
+        fixture = LEGACY_FIXTURES / (name + "_partial_yank" + ".html")
+
+        with fixture.open(encoding="utf-8") as f:
+            return [200, headers, f.read()]
+
+    http.register_uri(
+        http.GET,
+        re.compile("^https://foo2.bar/simple/(.+?)$"),
+        body=callback,
+    )
+
+
 @pytest.fixture()
 def pool() -> Pool:
     pool = Pool()
@@ -98,6 +118,9 @@ def pool() -> Pool:
     pool.add_repository(
         LegacyRepository("foo", "https://foo.bar/simple/", disable_cache=True)
     )
+    pool.add_repository(
+        LegacyRepository("foo2", "https://foo2.bar/simple/", disable_cache=True)
+    )
 
     return pool
 
@@ -266,6 +289,83 @@ def test_chooser_chooses_distributions_that_match_the_package_hashes(
     assert link.filename == "isort-4.3.4.tar.gz"
 
 
+@pytest.mark.parametrize("source_type", ["", "legacy"])
+def test_chooser_chooses_yanked_if_no_others(
+    env: MockEnv,
+    mock_pypi: None,
+    mock_legacy: None,
+    source_type: str,
+    pool: Pool,
+) -> None:
+    chooser = Chooser(pool, env)
+
+    package = Package("black", "21.11b0")
+    files = [
+        {
+            "filename": "black-21.11b0-py3-none-any.whl",
+            "hash": "sha256:0b1f66cbfadcd332ceeaeecf6373d9991d451868d2e2219ad0ac1213fb701117",  # noqa: E501
+        }
+    ]
+    if source_type == "legacy":
+        package = Package(
+            package.name,
+            package.version.text,
+            source_type="legacy",
+            source_reference="foo",
+            source_url="https://foo.bar/simple/",
+        )
+
+    package.files = files
+
+    link = chooser.choose_for(package)
+
+    assert link.filename == "black-21.11b0-py3-none-any.whl"
+    assert link.yanked
+
+
+def test_chooser_does_not_choose_yanked_if_others(
+    mock_legacy: None,
+    mock_legacy_partial_yank: None,
+    pool: Pool,
+) -> None:
+    chooser = Chooser(pool, MockEnv(supported_tags=[Tag("py2", "none", "any")]))
+
+    package = Package("futures", "3.2.0")
+    files = [
+        {
+            "filename": "futures-3.2.0-py2-none-any.whl",
+            "hash": "sha256:ec0a6cb848cc212002b9828c3e34c675e0c9ff6741dc445cab6fdd4e1085d1f1",  # noqa: E501
+        },
+        {
+            "filename": "futures-3.2.0.tar.gz",
+            "hash": "sha256:9ec02aa7d674acb8618afb127e27fde7fc68994c0437ad759fa094a574adb265",  # noqa: E501
+        },
+    ]
+    package = Package(
+        package.name,
+        package.version.text,
+        source_type="legacy",
+        source_reference="foo",
+        source_url="https://foo.bar/simple/",
+    )
+    package_partial_yank = Package(
+        package.name,
+        package.version.text,
+        source_type="legacy",
+        source_reference="foo2",
+        source_url="https://foo2.bar/simple/",
+    )
+
+    package.files = files
+    package_partial_yank.files = files
+
+    link = chooser.choose_for(package)
+    link_partial_yank = chooser.choose_for(package_partial_yank)
+
+    assert link.filename == "futures-3.2.0-py2-none-any.whl"
+    assert link_partial_yank.filename == "futures-3.2.0.tar.gz"
+
+
 @pytest.mark.parametrize("source_type", ["", "legacy"])
 def test_chooser_throws_an_error_if_package_hashes_do_not_match(
     env: MockEnv,

From d60a56de3d7f6e6a5b0dc2038f9ad674a4cd4239 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
 <30527984+radoering@users.noreply.github.com>
Date: Mon, 13 Jun 2022 17:45:36 +0200
Subject: [PATCH 6/6] installer: print warning if yanked file is used for
 install

---
 src/poetry/installation/executor.py | 17 +++++++++
 tests/installation/test_executor.py | 56 +++++++++++++++++++++++++++++
 2 files changed, 73 insertions(+)

diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py
index 4990ea26e4f..e3b56cf2a13 100644
--- a/src/poetry/installation/executor.py
+++ b/src/poetry/installation/executor.py
@@ -78,6 +78,7 @@ def __init__(
         self._executed = {"install": 0, "update": 0, "uninstall": 0}
         self._skipped = {"install": 0, "update": 0, "uninstall": 0}
         self._sections: dict[int, SectionOutput] = {}
+        self._yanked_warnings: list[str] = []
         self._lock = threading.Lock()
         self._shutdown = False
         self._hashes: dict[str, str] = {}
@@ -140,6 +141,7 @@ def execute(self, operations: list[Operation]) -> int:
         # We group operations by priority
         groups = itertools.groupby(operations, key=lambda o: -o.priority)
         self._sections = {}
+        self._yanked_warnings = []
         for _, group in groups:
             tasks = []
             serial_operations = []
@@ -179,6 +181,9 @@ def execute(self, operations: list[Operation]) -> int:
 
                 break
 
+        for warning in self._yanked_warnings:
+            self._io.write_error_line(f"<warning>Warning: {warning}</warning>")
+
         return 1 if self._shutdown else 0
 
     @staticmethod
@@ -611,6 +616,18 @@ def _install_git(self, operation: Install | Update) -> int:
     def _download(self, operation: Install | Update) -> Path:
         link = self._chooser.choose_for(operation.package)
 
+        if link.yanked:
+            # Store yanked warnings in a list and print after installing, so they can't
+            # be overlooked. Further, printing them in the concerning section would have
+            # the risk of overwriting the warning, so it is only briefly visible.
+            message = (
+                f"The file chosen for install of {operation.package.pretty_name} "
+                f"{operation.package.pretty_version} ({link.show_url}) is yanked."
+            )
+            if link.yanked_reason:
+                message += f" Reason for being yanked: {link.yanked_reason}"
+            self._yanked_warnings.append(message)
+
         return self._download_link(operation, link)
 
     def _download_link(self, operation: Install | Update, link: Link) -> Path:
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 7f60905a33c..edfb6c27f98 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -32,6 +32,7 @@
     from pytest_mock import MockerFixture
 
     from poetry.config.config import Config
+    from poetry.installation.operations.operation import Operation
     from poetry.utils.env import VirtualEnv
     from tests.types import FixtureDirGetter
 
@@ -177,6 +178,61 @@ def test_execute_executes_a_batch_of_operations(
     assert pip_install.call_args.kwargs.get("editable", False)
 
 
+@pytest.mark.parametrize(
+    "operations, has_warning",
+    [
+        (
+            [Install(Package("black", "21.11b0")), Install(Package("pytest", "3.5.2"))],
+            True,
+        ),
+        (
+            [
+                Uninstall(Package("black", "21.11b0")),
+                Uninstall(Package("pytest", "3.5.2")),
+            ],
+            False,
+        ),
+        (
+            [
+                Update(Package("black", "19.10b0"), Package("black", "21.11b0")),
+                Update(Package("pytest", "3.5.1"), Package("pytest", "3.5.2")),
+            ],
+            True,
+        ),
+    ],
+)
+def test_execute_prints_warning_for_yanked_package(
+    config: Config,
+    pool: Pool,
+    io: BufferedIO,
+    tmp_dir: str,
+    mock_file_downloads: None,
+    env: MockEnv,
+    operations: list[Operation],
+    has_warning: bool,
+):
+    config.merge({"cache-dir": tmp_dir})
+
+    executor = Executor(env, pool, config, io)
+
+    return_code = executor.execute(operations)
+
+    expected = (
+        "Warning: The file chosen for install of black 21.11b0 "
+        "(black-21.11b0-py3-none-any.whl) is yanked. Reason for being yanked: "
+        "Broken regex dependency. Use 21.11b1 instead."
+    )
+    error = io.fetch_error()
+    assert return_code == 0
+    assert "pytest" not in error
+    if has_warning:
+        assert expected in error
+        assert error.count("is yanked") == 1
+    else:
+        assert expected not in error
+        assert error.count("yanked") == 0
+
+
 def test_execute_shows_skipped_operations_if_verbose(
     config: Config, pool: Pool, io: BufferedIO, config_cache_dir: Path, env: MockEnv
 ):