We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Throw a RuntimeError on hash mismatch in Chooser._get_links
Throw a specific exception in the case of finding a matching name+version, but none of the digests for a link matching the `poetry.lock` metadata.

Resolves: #2422

- [✅] Added **tests** for changed code.
- [✅] Updated **documentation** for changed code.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `poetry/installation/chooser.py`
Content:
```
1 import re
2 
3 from typing import List
4 from typing import Tuple
5 
6 from packaging.tags import Tag
7 
8 from poetry.core.packages.package import Package
9 from poetry.core.packages.utils.link import Link
10 from poetry.repositories.pool import Pool
11 from poetry.utils.env import Env
12 from poetry.utils.patterns import wheel_file_re
13 
14 
15 class InvalidWheelName(Exception):
16     pass
17 
18 
19 class Wheel(object):
20     def __init__(self, filename):  # type: (str) -> None
21         wheel_info = wheel_file_re.match(filename)
22         if not wheel_info:
23             raise InvalidWheelName("{} is not a valid wheel filename.".format(filename))
24 
25         self.filename = filename
26         self.name = wheel_info.group("name").replace("_", "-")
27         self.version = wheel_info.group("ver").replace("_", "-")
28         self.build_tag = wheel_info.group("build")
29         self.pyversions = wheel_info.group("pyver").split(".")
30         self.abis = wheel_info.group("abi").split(".")
31         self.plats = wheel_info.group("plat").split(".")
32 
33         self.tags = {
34             Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
35         }
36 
37     def get_minimum_supported_index(self, tags):
38         indexes = [tags.index(t) for t in self.tags if t in tags]
39 
40         return min(indexes) if indexes else None
41 
42     def is_supported_by_environment(self, env):
43         return bool(set(env.supported_tags).intersection(self.tags))
44 
45 
46 class Chooser:
47     """
48     A Chooser chooses an appropriate release archive for packages.
49     """
50 
51     def __init__(self, pool, env):  # type: (Pool, Env) -> None
52         self._pool = pool
53         self._env = env
54 
55     def choose_for(self, package):  # type: (Package) -> Link
56         """
57         Return the url of the selected archive for a given package.
58         """
59         links = []
60         for link in self._get_links(package):
61             if link.is_wheel and not Wheel(link.filename).is_supported_by_environment(
62                 self._env
63             ):
64                 continue
65 
66             if link.ext in {".egg", ".exe", ".msi", ".rpm", ".srpm"}:
67                 continue
68 
69             links.append(link)
70 
71         if not links:
72             raise RuntimeError(
73                 "Unable to find installation candidates for {}".format(package)
74             )
75 
76         # Get the best link
77         chosen = max(links, key=lambda link: self._sort_key(package, link))
78         if not chosen:
79             raise RuntimeError(
80                 "Unable to find installation candidates for {}".format(package)
81             )
82 
83         return chosen
84 
85     def _get_links(self, package):  # type: (Package) -> List[Link]
86         if not package.source_type:
87             if not self._pool.has_repository("pypi"):
88                 repository = self._pool.repositories[0]
89             else:
90                 repository = self._pool.repository("pypi")
91         else:
92             repository = self._pool.repository(package.source_reference)
93 
94         links = repository.find_links_for_package(package)
95 
96         hashes = [f["hash"] for f in package.files]
97         if not hashes:
98             return links
99 
100         selected_links = []
101         for link in links:
102             if not link.hash:
103                 selected_links.append(link)
104                 continue
105 
106             h = link.hash_name + ":" + link.hash
107             if h not in hashes:
108                 continue
109 
110             selected_links.append(link)
111 
112         return selected_links
113 
114     def _sort_key(self, package, link):  # type: (Package, Link) -> Tuple
115         """
116         Function to pass as the `key` argument to a call to sorted() to sort
117         InstallationCandidates by preference.
118         Returns a tuple such that tuples sorting as greater using Python's
119         default comparison operator are more preferred.
120         The preference is as follows:
121         First and foremost, candidates with allowed (matching) hashes are
122         always preferred over candidates without matching hashes. This is
123         because e.g. if the only candidate with an allowed hash is yanked,
124         we still want to use that candidate.
125         Second, excepting hash considerations, candidates that have been
126         yanked (in the sense of PEP 592) are always less preferred than
127         candidates that haven't been yanked. Then:
128         If not finding wheels, they are sorted by version only.
129         If finding wheels, then the sort order is by version, then:
130           1. existing installs
131           2. wheels ordered via Wheel.support_index_min(self._supported_tags)
132           3. source archives
133         If prefer_binary was set, then all wheels are sorted above sources.
134         Note: it was considered to embed this logic into the Link
135               comparison operators, but then different sdist links
136               with the same version, would have to be considered equal
137         """
138         support_num = len(self._env.supported_tags)
139         build_tag = ()
140         binary_preference = 0
141         if link.is_wheel:
142             wheel = Wheel(link.filename)
143             if not wheel.is_supported_by_environment(self._env):
144                 raise RuntimeError(
145                     "{} is not a supported wheel for this platform. It "
146                     "can't be sorted.".format(wheel.filename)
147                 )
148 
149             # TODO: Binary preference
150             pri = -(wheel.get_minimum_supported_index(self._env.supported_tags))
151             if wheel.build_tag is not None:
152                 match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
153                 build_tag_groups = match.groups()
154                 build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
155         else:  # sdist
156             pri = -support_num
157 
158         has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package))
159 
160         # TODO: Proper yank value
161         yank_value = 0
162 
163         return (
164             has_allowed_hash,
165             yank_value,
166             binary_preference,
167             package.version,
168             build_tag,
169             pri,
170         )
171 
172     def _is_link_hash_allowed_for_package(
173         self, link, package
174     ):  # type: (Link, Package) -> bool
175         if not link.hash:
176             return True
177 
178         h = link.hash_name + ":" + link.hash
179 
180         return h in {f["hash"] for f in package.files}
181
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/poetry/installation/chooser.py b/poetry/installation/chooser.py
--- a/poetry/installation/chooser.py
+++ b/poetry/installation/chooser.py
@@ -109,6 +109,13 @@
 
             selected_links.append(link)
 
+        if links and not selected_links:
+            raise RuntimeError(
+                "Retrieved digest for link {}({}) not in poetry.lock metadata {}".format(
+                    link.filename, h, hashes
+                )
+            )
+
         return selected_links
 
     def _sort_key(self, package, link):  # type: (Package, Link) -> Tuple

{"golden_diff": "diff --git a/poetry/installation/chooser.py b/poetry/installation/chooser.py\n--- a/poetry/installation/chooser.py\n+++ b/poetry/installation/chooser.py\n@@ -109,6 +109,13 @@\n \n             selected_links.append(link)\n \n+        if links and not selected_links:\n+            raise RuntimeError(\n+                \"Retrieved digest for link {}({}) not in poetry.lock metadata {}\".format(\n+                    link.filename, h, hashes\n+                )\n+            )\n+\n         return selected_links\n \n     def _sort_key(self, package, link):  # type: (Package, Link) -> Tuple\n", "issue": "Throw a RuntimeError on hash mismatch in Chooser._get_links\nThrow a specific exception in the case of finding a matching name+version, but none of the digests for a link matching the `poetry.lock` metadata.\r\n\r\nResolves: #2422\r\n\r\n- [\u2705] Added **tests** for changed code.\r\n- [\u2705] Updated **documentation** for changed code.\r\n\n", "before_files": [{"content": "import re\n\nfrom typing import List\nfrom typing import Tuple\n\nfrom packaging.tags import Tag\n\nfrom poetry.core.packages.package import Package\nfrom poetry.core.packages.utils.link import Link\nfrom poetry.repositories.pool import Pool\nfrom poetry.utils.env import Env\nfrom poetry.utils.patterns import wheel_file_re\n\n\nclass InvalidWheelName(Exception):\n    pass\n\n\nclass Wheel(object):\n    def __init__(self, filename):  # type: (str) -> None\n        wheel_info = wheel_file_re.match(filename)\n        if not wheel_info:\n            raise InvalidWheelName(\"{} is not a valid wheel filename.\".format(filename))\n\n        self.filename = filename\n        self.name = wheel_info.group(\"name\").replace(\"_\", \"-\")\n        self.version = wheel_info.group(\"ver\").replace(\"_\", \"-\")\n        self.build_tag = wheel_info.group(\"build\")\n        self.pyversions = wheel_info.group(\"pyver\").split(\".\")\n        self.abis = wheel_info.group(\"abi\").split(\".\")\n        self.plats = wheel_info.group(\"plat\").split(\".\")\n\n        self.tags = {\n            Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats\n        }\n\n    def get_minimum_supported_index(self, tags):\n        indexes = [tags.index(t) for t in self.tags if t in tags]\n\n        return min(indexes) if indexes else None\n\n    def is_supported_by_environment(self, env):\n        return bool(set(env.supported_tags).intersection(self.tags))\n\n\nclass Chooser:\n    \"\"\"\n    A Chooser chooses an appropriate release archive for packages.\n    \"\"\"\n\n    def __init__(self, pool, env):  # type: (Pool, Env) -> None\n        self._pool = pool\n        self._env = env\n\n    def choose_for(self, package):  # type: (Package) -> Link\n        \"\"\"\n        Return the url of the selected archive for a given package.\n        \"\"\"\n        links = []\n        for link in self._get_links(package):\n            if link.is_wheel and not Wheel(link.filename).is_supported_by_environment(\n                self._env\n            ):\n                continue\n\n            if link.ext in {\".egg\", \".exe\", \".msi\", \".rpm\", \".srpm\"}:\n                continue\n\n            links.append(link)\n\n        if not links:\n            raise RuntimeError(\n                \"Unable to find installation candidates for {}\".format(package)\n            )\n\n        # Get the best link\n        chosen = max(links, key=lambda link: self._sort_key(package, link))\n        if not chosen:\n            raise RuntimeError(\n                \"Unable to find installation candidates for {}\".format(package)\n            )\n\n        return chosen\n\n    def _get_links(self, package):  # type: (Package) -> List[Link]\n        if not package.source_type:\n            if not self._pool.has_repository(\"pypi\"):\n                repository = self._pool.repositories[0]\n            else:\n                repository = self._pool.repository(\"pypi\")\n        else:\n            repository = self._pool.repository(package.source_reference)\n\n        links = repository.find_links_for_package(package)\n\n        hashes = [f[\"hash\"] for f in package.files]\n        if not hashes:\n            return links\n\n        selected_links = []\n        for link in links:\n            if not link.hash:\n                selected_links.append(link)\n                continue\n\n            h = link.hash_name + \":\" + link.hash\n            if h not in hashes:\n                continue\n\n            selected_links.append(link)\n\n        return selected_links\n\n    def _sort_key(self, package, link):  # type: (Package, Link) -> Tuple\n        \"\"\"\n        Function to pass as the `key` argument to a call to sorted() to sort\n        InstallationCandidates by preference.\n        Returns a tuple such that tuples sorting as greater using Python's\n        default comparison operator are more preferred.\n        The preference is as follows:\n        First and foremost, candidates with allowed (matching) hashes are\n        always preferred over candidates without matching hashes. This is\n        because e.g. if the only candidate with an allowed hash is yanked,\n        we still want to use that candidate.\n        Second, excepting hash considerations, candidates that have been\n        yanked (in the sense of PEP 592) are always less preferred than\n        candidates that haven't been yanked. Then:\n        If not finding wheels, they are sorted by version only.\n        If finding wheels, then the sort order is by version, then:\n          1. existing installs\n          2. wheels ordered via Wheel.support_index_min(self._supported_tags)\n          3. source archives\n        If prefer_binary was set, then all wheels are sorted above sources.\n        Note: it was considered to embed this logic into the Link\n              comparison operators, but then different sdist links\n              with the same version, would have to be considered equal\n        \"\"\"\n        support_num = len(self._env.supported_tags)\n        build_tag = ()\n        binary_preference = 0\n        if link.is_wheel:\n            wheel = Wheel(link.filename)\n            if not wheel.is_supported_by_environment(self._env):\n                raise RuntimeError(\n                    \"{} is not a supported wheel for this platform. It \"\n                    \"can't be sorted.\".format(wheel.filename)\n                )\n\n            # TODO: Binary preference\n            pri = -(wheel.get_minimum_supported_index(self._env.supported_tags))\n            if wheel.build_tag is not None:\n                match = re.match(r\"^(\\d+)(.*)$\", wheel.build_tag)\n                build_tag_groups = match.groups()\n                build_tag = (int(build_tag_groups[0]), build_tag_groups[1])\n        else:  # sdist\n            pri = -support_num\n\n        has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package))\n\n        # TODO: Proper yank value\n        yank_value = 0\n\n        return (\n            has_allowed_hash,\n            yank_value,\n            binary_preference,\n            package.version,\n            build_tag,\n            pri,\n        )\n\n    def _is_link_hash_allowed_for_package(\n        self, link, package\n    ):  # type: (Link, Package) -> bool\n        if not link.hash:\n            return True\n\n        h = link.hash_name + \":\" + link.hash\n\n        return h in {f[\"hash\"] for f in package.files}\n", "path": "poetry/installation/chooser.py"}], "after_files": [{"content": "import re\n\nfrom typing import List\nfrom typing import Tuple\n\nfrom packaging.tags import Tag\n\nfrom poetry.core.packages.package import Package\nfrom poetry.core.packages.utils.link import Link\nfrom poetry.repositories.pool import Pool\nfrom poetry.utils.env import Env\nfrom poetry.utils.patterns import wheel_file_re\n\n\nclass InvalidWheelName(Exception):\n    pass\n\n\nclass Wheel(object):\n    def __init__(self, filename):  # type: (str) -> None\n        wheel_info = wheel_file_re.match(filename)\n        if not wheel_info:\n            raise InvalidWheelName(\"{} is not a valid wheel filename.\".format(filename))\n\n        self.filename = filename\n        self.name = wheel_info.group(\"name\").replace(\"_\", \"-\")\n        self.version = wheel_info.group(\"ver\").replace(\"_\", \"-\")\n        self.build_tag = wheel_info.group(\"build\")\n        self.pyversions = wheel_info.group(\"pyver\").split(\".\")\n        self.abis = wheel_info.group(\"abi\").split(\".\")\n        self.plats = wheel_info.group(\"plat\").split(\".\")\n\n        self.tags = {\n            Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats\n        }\n\n    def get_minimum_supported_index(self, tags):\n        indexes = [tags.index(t) for t in self.tags if t in tags]\n\n        return min(indexes) if indexes else None\n\n    def is_supported_by_environment(self, env):\n        return bool(set(env.supported_tags).intersection(self.tags))\n\n\nclass Chooser:\n    \"\"\"\n    A Chooser chooses an appropriate release archive for packages.\n    \"\"\"\n\n    def __init__(self, pool, env):  # type: (Pool, Env) -> None\n        self._pool = pool\n        self._env = env\n\n    def choose_for(self, package):  # type: (Package) -> Link\n        \"\"\"\n        Return the url of the selected archive for a given package.\n        \"\"\"\n        links = []\n        for link in self._get_links(package):\n            if link.is_wheel and not Wheel(link.filename).is_supported_by_environment(\n                self._env\n            ):\n                continue\n\n            if link.ext in {\".egg\", \".exe\", \".msi\", \".rpm\", \".srpm\"}:\n                continue\n\n            links.append(link)\n\n        if not links:\n            raise RuntimeError(\n                \"Unable to find installation candidates for {}\".format(package)\n            )\n\n        # Get the best link\n        chosen = max(links, key=lambda link: self._sort_key(package, link))\n        if not chosen:\n            raise RuntimeError(\n                \"Unable to find installation candidates for {}\".format(package)\n            )\n\n        return chosen\n\n    def _get_links(self, package):  # type: (Package) -> List[Link]\n        if not package.source_type:\n            if not self._pool.has_repository(\"pypi\"):\n                repository = self._pool.repositories[0]\n            else:\n                repository = self._pool.repository(\"pypi\")\n        else:\n            repository = self._pool.repository(package.source_reference)\n\n        links = repository.find_links_for_package(package)\n\n        hashes = [f[\"hash\"] for f in package.files]\n        if not hashes:\n            return links\n\n        selected_links = []\n        for link in links:\n            if not link.hash:\n                selected_links.append(link)\n                continue\n\n            h = link.hash_name + \":\" + link.hash\n            if h not in hashes:\n                continue\n\n            selected_links.append(link)\n\n        if links and not selected_links:\n            raise RuntimeError(\n                \"Retrieved digest for link {}({}) not in poetry.lock metadata {}\".format(\n                    link.filename, h, hashes\n                )\n            )\n\n        return selected_links\n\n    def _sort_key(self, package, link):  # type: (Package, Link) -> Tuple\n        \"\"\"\n        Function to pass as the `key` argument to a call to sorted() to sort\n        InstallationCandidates by preference.\n        Returns a tuple such that tuples sorting as greater using Python's\n        default comparison operator are more preferred.\n        The preference is as follows:\n        First and foremost, candidates with allowed (matching) hashes are\n        always preferred over candidates without matching hashes. This is\n        because e.g. if the only candidate with an allowed hash is yanked,\n        we still want to use that candidate.\n        Second, excepting hash considerations, candidates that have been\n        yanked (in the sense of PEP 592) are always less preferred than\n        candidates that haven't been yanked. Then:\n        If not finding wheels, they are sorted by version only.\n        If finding wheels, then the sort order is by version, then:\n          1. existing installs\n          2. wheels ordered via Wheel.support_index_min(self._supported_tags)\n          3. source archives\n        If prefer_binary was set, then all wheels are sorted above sources.\n        Note: it was considered to embed this logic into the Link\n              comparison operators, but then different sdist links\n              with the same version, would have to be considered equal\n        \"\"\"\n        support_num = len(self._env.supported_tags)\n        build_tag = ()\n        binary_preference = 0\n        if link.is_wheel:\n            wheel = Wheel(link.filename)\n            if not wheel.is_supported_by_environment(self._env):\n                raise RuntimeError(\n                    \"{} is not a supported wheel for this platform. It \"\n                    \"can't be sorted.\".format(wheel.filename)\n                )\n\n            # TODO: Binary preference\n            pri = -(wheel.get_minimum_supported_index(self._env.supported_tags))\n            if wheel.build_tag is not None:\n                match = re.match(r\"^(\\d+)(.*)$\", wheel.build_tag)\n                build_tag_groups = match.groups()\n                build_tag = (int(build_tag_groups[0]), build_tag_groups[1])\n        else:  # sdist\n            pri = -support_num\n\n        has_allowed_hash = int(self._is_link_hash_allowed_for_package(link, package))\n\n        # TODO: Proper yank value\n        yank_value = 0\n\n        return (\n            has_allowed_hash,\n            yank_value,\n            binary_preference,\n            package.version,\n            build_tag,\n            pri,\n        )\n\n    def _is_link_hash_allowed_for_package(\n        self, link, package\n    ):  # type: (Link, Package) -> bool\n        if not link.hash:\n            return True\n\n        h = link.hash_name + \":\" + link.hash\n\n        return h in {f[\"hash\"] for f in package.files}\n", "path": "poetry/installation/chooser.py"}]}