Skip to content

Commit 2c277d2

Browse files
authored
Remove crashtest (#382)
1 parent f947528 commit 2c277d2

File tree

15 files changed

+362
-188
lines changed

15 files changed

+362
-188
lines changed

news/382.deps.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Removed `crashtest` dependency and vendored part of it into `cleo`

poetry.lock

Lines changed: 1 addition & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ classifiers = [
3434

3535
[tool.poetry.dependencies]
3636
python = "^3.8"
37-
crashtest = "^0.4.1"
3837
rapidfuzz = "^3.0.0"
3938

4039
[tool.poetry.group.dev.dependencies]

src/cleo/application.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@
3434

3535

3636
if TYPE_CHECKING:
37-
from crashtest.solution_providers.solution_provider_repository import (
38-
SolutionProviderRepository,
39-
)
40-
4137
from cleo.commands.command import Command
4238
from cleo.events.event_dispatcher import EventDispatcher
4339
from cleo.io.inputs.input import Input
@@ -78,8 +74,6 @@ def __init__(self, name: str = "console", version: str = "") -> None:
7874

7975
self._command_loader: CommandLoader | None = None
8076

81-
self._solution_provider_repository: SolutionProviderRepository | None = None
82-
8377
@property
8478
def name(self) -> str:
8579
return self._name
@@ -170,11 +164,6 @@ def catch_exceptions(self, catch_exceptions: bool = True) -> None:
170164
def is_single_command(self) -> bool:
171165
return self._single_command
172166

173-
def set_solution_provider_repository(
174-
self, solution_provider_repository: SolutionProviderRepository
175-
) -> None:
176-
self._solution_provider_repository = solution_provider_repository
177-
178167
def add(self, command: Command) -> Command | None:
179168
self._init()
180169

@@ -493,11 +482,9 @@ def create_io(
493482
return IO(input, output, error_output)
494483

495484
def render_error(self, error: Exception, io: IO) -> None:
496-
from cleo.ui.exception_trace import ExceptionTrace
485+
from cleo.ui.exception_trace.component import ExceptionTrace
497486

498-
trace = ExceptionTrace(
499-
error, solution_provider_repository=self._solution_provider_repository
500-
)
487+
trace = ExceptionTrace(error)
501488
simple = not io.is_verbose() or isinstance(error, CleoUserError)
502489
trace.render(io.error_output, simple)
503490

src/cleo/ui/exception_trace/__init__.py

Whitespace-only changes.

src/cleo/ui/exception_trace.py renamed to src/cleo/ui/exception_trace/component.py

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,18 @@
1010
import sys
1111
import tokenize
1212

13+
from pathlib import Path
1314
from typing import TYPE_CHECKING
1415
from typing import ClassVar
1516

16-
from crashtest.frame_collection import FrameCollection
17-
1817
from cleo.formatters.formatter import Formatter
18+
from cleo.ui.exception_trace.frame_collection import FrameCollection
1919

2020

2121
if TYPE_CHECKING:
22-
from crashtest.frame import Frame
23-
from crashtest.solution_providers.solution_provider_repository import (
24-
SolutionProviderRepository,
25-
)
26-
2722
from cleo.io.io import IO
2823
from cleo.io.outputs.output import Output
24+
from cleo.ui.exception_trace.frame import Frame
2925

3026

3127
class Highlighter:
@@ -231,10 +227,8 @@ class ExceptionTrace:
231227
def __init__(
232228
self,
233229
exception: Exception,
234-
solution_provider_repository: SolutionProviderRepository | None = None,
235230
) -> None:
236231
self._exception = exception
237-
self._solution_provider_repository = solution_provider_repository
238232
self._exc_info = sys.exc_info()
239233
self._ignore: str | None = None
240234

@@ -252,10 +246,8 @@ def render(self, io: IO | Output, simple: bool = False) -> None:
252246
else:
253247
self._render_exception(io, self._exception)
254248

255-
self._render_solution(io, self._exception)
256-
257249
def _render_exception(self, io: IO | Output, exception: BaseException) -> None:
258-
from crashtest.inspector import Inspector
250+
from cleo.ui.exception_trace.inspector import Inspector
259251

260252
inspector = Inspector(exception)
261253
if not inspector.frames:
@@ -297,31 +289,6 @@ def _render_snippet(self, io: IO | Output, frame: Frame) -> None:
297289
for code_line in code_lines:
298290
self._render_line(io, code_line, indent=4)
299291

300-
def _render_solution(self, io: IO | Output, exception: Exception) -> None:
301-
if self._solution_provider_repository is None:
302-
return
303-
304-
solutions = self._solution_provider_repository.get_solutions_for_exception(
305-
exception
306-
)
307-
symbol = "•" if io.supports_utf8() else "*"
308-
309-
for solution in solutions:
310-
title = solution.solution_title
311-
description = solution.solution_description
312-
links = solution.documentation_links
313-
314-
description = description.replace("\n", "\n ").strip(" ")
315-
316-
joined_links = ",".join(f"\n <fg=blue>{link}</>" for link in links)
317-
self._render_line(
318-
io,
319-
f"<fg=blue;options=bold>{symbol} </>"
320-
f"<fg=default;options=bold>{title.rstrip('.')}</>:"
321-
f" {description}{joined_links}",
322-
True,
323-
)
324-
325292
def _render_trace(self, io: IO | Output, frames: FrameCollection) -> None:
326293
stack_frames = FrameCollection()
327294
for frame in frames:
@@ -341,7 +308,7 @@ def _render_trace(self, io: IO | Output, frames: FrameCollection) -> None:
341308
frame_collections = stack_frames.compact()
342309
i = remaining_frames_length
343310
for collection in frame_collections:
344-
if collection.is_repeated():
311+
if collection.is_repeated:
345312
if len(collection) > 1:
346313
frames_message = f"<fg=yellow>{len(collection)}</> frames"
347314
else:
@@ -359,7 +326,7 @@ def _render_trace(self, io: IO | Output, frames: FrameCollection) -> None:
359326

360327
for frame in collection:
361328
relative_file_path = self._get_relative_file_path(frame.filename)
362-
relative_file_path_parts = relative_file_path.split(os.path.sep)
329+
relative_file_path_parts = relative_file_path.split(os.sep)
363330
relative_file_path = (
364331
f"<fg=default;options=dark>{Formatter.escape(os.sep)}</>".join(
365332
relative_file_path_parts[:-1]
@@ -411,22 +378,21 @@ def _render_trace(self, io: IO | Output, frames: FrameCollection) -> None:
411378

412379
i -= 1
413380

381+
@staticmethod
414382
def _render_line(
415-
self, io: IO | Output, line: str, new_line: bool = False, indent: int = 2
383+
io: IO | Output, line: str, new_line: bool = False, indent: int = 2
416384
) -> None:
417385
if new_line:
418386
io.write_line("")
419387

420388
io.write_line(f"{indent * ' '}{line}")
421389

422-
def _get_relative_file_path(self, filepath: str) -> str:
423-
cwd = os.getcwd()
424-
425-
if cwd:
426-
filepath = filepath.replace(cwd + os.path.sep, "")
390+
@staticmethod
391+
def _get_relative_file_path(filepath: str) -> str:
392+
if cwd := Path.cwd():
393+
filepath = filepath.replace(f"{cwd}{os.sep}", "")
427394

428-
home = os.path.expanduser("~")
429-
if home:
430-
filepath = filepath.replace(home + os.path.sep, "~" + os.path.sep)
395+
if home := Path("~").expanduser():
396+
filepath = filepath.replace(f"{home}{os.sep}", f"~{os.sep}")
431397

432398
return filepath
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from __future__ import annotations
2+
3+
import operator
4+
5+
from functools import reduce
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING
8+
from typing import ClassVar
9+
10+
11+
if TYPE_CHECKING:
12+
import inspect
13+
14+
from types import FrameType
15+
16+
17+
class Frame:
18+
_content_cache: ClassVar[dict[str, str]] = {}
19+
20+
def __init__(self, frame_info: inspect.FrameInfo) -> None:
21+
self._frame = frame_info.frame
22+
self._frame_info = frame_info
23+
self._lineno = frame_info.lineno
24+
self._filename = frame_info.filename
25+
self._function = frame_info.function
26+
self._lines = None
27+
self._file_content: str | None = None
28+
29+
@property
30+
def frame(self) -> FrameType:
31+
return self._frame
32+
33+
@property
34+
def lineno(self) -> int:
35+
return self._lineno
36+
37+
@property
38+
def filename(self) -> str:
39+
return self._filename
40+
41+
@property
42+
def function(self) -> str:
43+
return self._function
44+
45+
@property
46+
def line(self) -> str:
47+
if not self._frame_info.code_context:
48+
return ""
49+
50+
return self._frame_info.code_context[0]
51+
52+
@property
53+
def _key(self) -> tuple[str, str, int]:
54+
return self._filename, self._function, self._lineno
55+
56+
@property
57+
def file_content(self) -> str:
58+
if self._file_content is not None:
59+
return self._file_content
60+
if not self._filename:
61+
self._file_content = ""
62+
return ""
63+
if self._filename not in type(self)._content_cache:
64+
try:
65+
file_content = Path(self._filename).read_text()
66+
except OSError:
67+
file_content = ""
68+
type(self)._content_cache[self._filename] = file_content
69+
self._file_content = type(self)._content_cache[self._filename]
70+
return self._file_content
71+
72+
def __hash__(self) -> int:
73+
return reduce(operator.xor, map(hash, self._key))
74+
75+
def __eq__(self, other: object) -> bool:
76+
if not isinstance(other, Frame):
77+
return NotImplemented
78+
return self._key == other._key
79+
80+
def __repr__(self) -> str:
81+
return f"<Frame {self._filename}, {self._function}, {self._lineno}>"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from __future__ import annotations
2+
3+
from typing import List
4+
5+
from cleo.ui.exception_trace.frame import Frame
6+
7+
8+
class FrameCollection(List[Frame]):
9+
def __init__(self, frames: list[Frame] | None = None, count: int = 0) -> None:
10+
if frames is None:
11+
frames = []
12+
13+
super().__init__(frames)
14+
15+
self._count = count
16+
17+
@property
18+
def repetitions(self) -> int:
19+
return self._count - 1
20+
21+
@property
22+
def is_repeated(self) -> bool:
23+
return self._count > 1
24+
25+
def increment_count(self, increment: int = 1) -> FrameCollection:
26+
self._count += increment
27+
28+
return self
29+
30+
def compact(self) -> list[FrameCollection]:
31+
"""
32+
Compacts the frames to deduplicate recursive calls.
33+
"""
34+
collections = []
35+
current_collection = FrameCollection()
36+
37+
i = 0
38+
while i < len(self) - 1:
39+
frame = self[i]
40+
if frame in self[i + 1 :]:
41+
duplicate_indices = []
42+
for sub_index, sub_frame in enumerate(self[i + 1 :]):
43+
if frame == sub_frame:
44+
duplicate_indices.append(sub_index + i + 1)
45+
46+
found_duplicate = False
47+
for duplicate_index in duplicate_indices:
48+
collection = FrameCollection(self[i:duplicate_index])
49+
if collection == current_collection:
50+
current_collection.increment_count()
51+
i = duplicate_index
52+
found_duplicate = True
53+
break
54+
55+
if found_duplicate:
56+
continue
57+
58+
collections.append(current_collection)
59+
current_collection = FrameCollection(self[i : duplicate_indices[0]])
60+
61+
i = duplicate_indices[0]
62+
63+
continue
64+
65+
if current_collection.is_repeated:
66+
collections.append(current_collection)
67+
current_collection = FrameCollection()
68+
69+
current_collection.append(frame)
70+
i += 1
71+
72+
collections.append(current_collection)
73+
74+
return collections

0 commit comments

Comments
 (0)