Skip to content

Commit f30b118

Browse files
committed
feat(ux): add error better ux for hash not found
This changes introduces the use of `PoetryRuntimeError` that allows better error information propagation to facilitate improved ux for users encountering errors. Resolves: #10057
1 parent 1b04d3c commit f30b118

File tree

4 files changed

+92
-7
lines changed

4 files changed

+92
-7
lines changed

src/poetry/console/exceptions.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,65 @@
11
from __future__ import annotations
22

3+
import dataclasses
4+
5+
from typing import TYPE_CHECKING
6+
37
from cleo.exceptions import CleoError
48

59

10+
if TYPE_CHECKING:
11+
from cleo.io.io import IO
12+
13+
614
class PoetryConsoleError(CleoError):
715
pass
816

917

1018
class GroupNotFoundError(PoetryConsoleError):
1119
pass
20+
21+
22+
@dataclasses.dataclass
23+
class ConsoleMessage:
24+
text: str
25+
debug: bool = False
26+
27+
@property
28+
def stripped(self) -> str:
29+
from cleo._utils import strip_tags
30+
31+
return strip_tags(self.text)
32+
33+
34+
class PoetryRuntimeError(PoetryConsoleError):
35+
def __init__(
36+
self,
37+
reason: str,
38+
messages: list[ConsoleMessage] | None = None,
39+
exit_code: int = 1,
40+
) -> None:
41+
super().__init__(reason)
42+
self.exit_code = exit_code
43+
self._messages = messages or []
44+
self._messages.insert(0, ConsoleMessage(reason + ("\n" if messages else "")))
45+
46+
def write(self, io: IO) -> None:
47+
io.write_error_line(self.get_text(debug=io.is_verbose(), strip=False))
48+
49+
def get_text(
50+
self, debug: bool = False, indent: str = "", strip: bool = False
51+
) -> str:
52+
text = ""
53+
for message in self._messages:
54+
if message.debug and not debug:
55+
continue
56+
57+
message_text = message.stripped if strip else message.text
58+
if indent:
59+
message_text = f"\n{indent}".join(message_text.splitlines())
60+
text += f"{indent}{message_text}\n{indent}\n"
61+
62+
return text.rstrip(f"{indent}\n")
63+
64+
def __str__(self) -> str:
65+
return self._messages[0].stripped.strip()

src/poetry/installation/chooser.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from poetry.config.config import Config
1010
from poetry.config.config import PackageFilterPolicy
11+
from poetry.console.exceptions import ConsoleMessage
12+
from poetry.console.exceptions import PoetryRuntimeError
1113
from poetry.repositories.http_repository import HTTPRepository
1214
from poetry.utils.helpers import get_highest_priority_hash_type
1315
from poetry.utils.wheel import Wheel
@@ -134,11 +136,28 @@ def _get_links(self, package: Package) -> list[Link]:
134136
selected_links.append(link)
135137

136138
if links and not selected_links:
137-
links_str = ", ".join(f"{link}({h})" for link, h in skipped)
138-
raise RuntimeError(
139-
f"Retrieved digests for links {links_str} not in poetry.lock"
140-
f" metadata {locked_hashes}"
141-
)
139+
reason = f"Downloaded distributions for <b>{package.pretty_name} ({package.pretty_version})</> did not match any known checksums in your lock file."
140+
link_hashes = "\n".join(f" - {link}({h})" for link, h in skipped)
141+
known_hashes = "\n".join(f" - {h}" for h in locked_hashes)
142+
messages = [
143+
ConsoleMessage(
144+
"<options=bold>Causes:</>\n"
145+
" - invalid or corrupt cache either during locking or installation\n"
146+
" - network interruptions or errors causing corrupted downloads\n\n"
147+
"<b>Solutions:</>\n"
148+
" 1. Try running your command again using the <c1>--no-cache</> global option enabled.\n"
149+
" 2. Try regenerating your lock file using (<c1>poetry lock --no-cache --regenerate</>).\n\n"
150+
"If any of those solutions worked, you will have to clear your caches using (<c1>poetry cache clear --all CACHE_NAME</>)."
151+
),
152+
ConsoleMessage(
153+
f"Poetry retrieved the following links:\n"
154+
f"{link_hashes}\n\n"
155+
f"The lockfile contained only the following hashes:\n"
156+
f"{known_hashes}",
157+
debug=True,
158+
),
159+
]
160+
raise PoetryRuntimeError(reason, messages)
142161

143162
return selected_links
144163

src/poetry/installation/executor.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from poetry.core.packages.utils.link import Link
1818

19+
from poetry.console.exceptions import PoetryRuntimeError
1920
from poetry.installation.chef import Chef
2021
from poetry.installation.chooser import Chooser
2122
from poetry.installation.operations import Install
@@ -333,6 +334,10 @@ def _execute_operation(self, operation: Operation) -> None:
333334
f" for {pkg.pretty_name}."
334335
"</error>"
335336
)
337+
elif isinstance(e, PoetryRuntimeError):
338+
message = e.get_text(io.is_verbose(), indent=" | ").rstrip()
339+
message = f"<warning>{message}</>"
340+
with_trace = False
336341
else:
337342
message = f"<error>Cannot install {pkg.pretty_name}.</error>"
338343

tests/installation/test_chooser.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from packaging.tags import Tag
99
from poetry.core.packages.package import Package
1010

11+
from poetry.console.exceptions import PoetryRuntimeError
1112
from poetry.installation.chooser import Chooser
1213
from poetry.repositories.legacy_repository import LegacyRepository
1314
from poetry.repositories.pypi_repository import PyPiRepository
@@ -366,9 +367,15 @@ def test_chooser_throws_an_error_if_package_hashes_do_not_match(
366367

367368
package.files = files
368369

369-
with pytest.raises(RuntimeError) as e:
370+
with pytest.raises(PoetryRuntimeError) as e:
370371
chooser.choose_for(package)
371-
assert files[0]["hash"] in str(e)
372+
373+
reason = f"Downloaded distributions for {package.name} ({package.version}) did not match any known checksums in your lock file."
374+
assert str(e.value) == reason
375+
376+
text = e.value.get_text(debug=True, strip=True)
377+
assert reason in text
378+
assert files[0]["hash"] in text
372379

373380

374381
def test_chooser_md5_remote_fallback_to_sha256_inline_calculation(

0 commit comments

Comments
 (0)