beautiful-traceback 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- beautiful_traceback/__init__.py +4 -9
- beautiful_traceback/cli.py +11 -0
- beautiful_traceback/common.py +47 -5
- beautiful_traceback/formatting.py +33 -18
- beautiful_traceback/json_formatting.py +204 -0
- beautiful_traceback/parsing.py +18 -11
- beautiful_traceback/pytest_plugin.py +43 -0
- {beautiful_traceback-0.2.0.dist-info → beautiful_traceback-0.3.0.dist-info}/METADATA +1 -1
- beautiful_traceback-0.3.0.dist-info/RECORD +13 -0
- {beautiful_traceback-0.2.0.dist-info → beautiful_traceback-0.3.0.dist-info}/WHEEL +2 -2
- beautiful_traceback-0.2.0.dist-info/RECORD +0 -12
- {beautiful_traceback-0.2.0.dist-info → beautiful_traceback-0.3.0.dist-info}/entry_points.txt +0 -0
beautiful_traceback/__init__.py
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
from .hook import install
|
|
2
|
-
from .hook import uninstall
|
|
3
|
-
from .formatting import LoggingFormatter
|
|
4
|
-
from .formatting import LoggingFormatterMixin
|
|
5
|
-
|
|
6
1
|
from ._extension import load_ipython_extension # noqa: F401
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
from .formatting import LoggingFormatter, LoggingFormatterMixin
|
|
3
|
+
from .hook import install, uninstall
|
|
4
|
+
from .json_formatting import exc_to_json
|
|
10
5
|
|
|
11
6
|
# retain typo for backward compatibility
|
|
12
7
|
LoggingFormaterMixin = LoggingFormatterMixin
|
|
@@ -15,8 +10,8 @@ LoggingFormaterMixin = LoggingFormatterMixin
|
|
|
15
10
|
__all__ = [
|
|
16
11
|
"install",
|
|
17
12
|
"uninstall",
|
|
18
|
-
"__version__",
|
|
19
13
|
"LoggingFormatter",
|
|
20
14
|
"LoggingFormatterMixin",
|
|
21
15
|
"LoggingFormaterMixin",
|
|
16
|
+
"exc_to_json",
|
|
22
17
|
]
|
beautiful_traceback/cli.py
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
import colorama
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def inject_pth() -> None:
|
|
8
10
|
"""
|
|
@@ -50,6 +52,15 @@ def _get_site_packages() -> Path:
|
|
|
50
52
|
|
|
51
53
|
def _create_injection_files(py_file: Path, pth_file: Path) -> None:
|
|
52
54
|
"""Create the Python injection file and .pth file."""
|
|
55
|
+
# Check if files already exist
|
|
56
|
+
if py_file.exists() or pth_file.exists():
|
|
57
|
+
colorama.init()
|
|
58
|
+
try:
|
|
59
|
+
warning_msg = f"{colorama.Fore.RED}Warning: Beautiful traceback injection already exists. Overwriting...{colorama.Style.RESET_ALL}"
|
|
60
|
+
print(warning_msg, file=sys.stderr)
|
|
61
|
+
finally:
|
|
62
|
+
colorama.deinit()
|
|
63
|
+
|
|
53
64
|
py_content = """def run_startup_script():
|
|
54
65
|
try:
|
|
55
66
|
import beautiful_traceback
|
beautiful_traceback/common.py
CHANGED
|
@@ -1,28 +1,70 @@
|
|
|
1
|
+
"""Common data structures and constants for beautiful-traceback.
|
|
2
|
+
|
|
3
|
+
This module provides an Internal Representation (IR) used to normalize Python's
|
|
4
|
+
complex exception graph into a simplified, flat format suitable for both
|
|
5
|
+
text and JSON rendering.
|
|
6
|
+
|
|
7
|
+
These structures:
|
|
8
|
+
1. Prevent circular imports between formatting modules.
|
|
9
|
+
2. Bundle exceptions with their specific traceback entries.
|
|
10
|
+
3. Flatten recursive exception chains (__cause__, __context__) into linear lists.
|
|
11
|
+
"""
|
|
12
|
+
|
|
1
13
|
import typing as typ
|
|
2
14
|
|
|
3
15
|
|
|
4
|
-
class
|
|
16
|
+
class StackFrameEntry(typ.NamedTuple):
|
|
17
|
+
"""A normalized stack frame.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
module: The path to the source file.
|
|
21
|
+
call: The name of the function or scope.
|
|
22
|
+
lineno: The line number as a string.
|
|
23
|
+
src_ctx: The source code line content.
|
|
24
|
+
"""
|
|
25
|
+
|
|
5
26
|
module: str
|
|
6
27
|
call: str
|
|
7
28
|
lineno: str
|
|
8
29
|
src_ctx: str
|
|
9
30
|
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
StackFrameEntryList = typ.List[StackFrameEntry]
|
|
33
|
+
|
|
12
34
|
|
|
35
|
+
class ExceptionTraceback(typ.NamedTuple):
|
|
36
|
+
"""A normalized representation of a single exception and its stack.
|
|
37
|
+
|
|
38
|
+
This bundles the exception metadata with its specific traceback entries,
|
|
39
|
+
and explicitly marks its relationship to other exceptions in a chain.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
exc_name: The class name of the exception.
|
|
43
|
+
exc_msg: The string representation of the exception.
|
|
44
|
+
stack_frames: A list of stack frames.
|
|
45
|
+
is_caused: True if this exception was the direct cause (__cause__).
|
|
46
|
+
is_context: True if this exception occurred during handling (__context__).
|
|
47
|
+
"""
|
|
13
48
|
|
|
14
|
-
class Traceback(typ.NamedTuple):
|
|
15
49
|
exc_name: str
|
|
16
50
|
exc_msg: str
|
|
17
|
-
|
|
51
|
+
stack_frames: StackFrameEntryList
|
|
18
52
|
|
|
19
53
|
is_caused: bool
|
|
20
54
|
is_context: bool
|
|
21
55
|
|
|
22
56
|
|
|
23
|
-
|
|
57
|
+
ExceptionTracebackList = typ.List[ExceptionTraceback]
|
|
24
58
|
|
|
59
|
+
# Standard headers used across different renderers
|
|
25
60
|
ALIASES_HEAD = "Aliases for entries in sys.path:"
|
|
61
|
+
"Header shown before the list of path aliases."
|
|
62
|
+
|
|
26
63
|
TRACEBACK_HEAD = "Traceback (most recent call last):"
|
|
64
|
+
"Standard Python header for a traceback."
|
|
65
|
+
|
|
27
66
|
CAUSE_HEAD = "The above exception was the direct cause of the following exception:"
|
|
67
|
+
"Header shown when an exception has an explicit __cause__."
|
|
68
|
+
|
|
28
69
|
CONTEXT_HEAD = "During handling of the above exception, another exception occurred:"
|
|
70
|
+
"Header shown when an exception has an implicit __context__."
|
|
@@ -10,7 +10,15 @@ import collections
|
|
|
10
10
|
|
|
11
11
|
import colorama
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
from beautiful_traceback.common import (
|
|
14
|
+
ALIASES_HEAD,
|
|
15
|
+
CAUSE_HEAD,
|
|
16
|
+
CONTEXT_HEAD,
|
|
17
|
+
TRACEBACK_HEAD,
|
|
18
|
+
ExceptionTraceback,
|
|
19
|
+
StackFrameEntry,
|
|
20
|
+
StackFrameEntryList,
|
|
21
|
+
)
|
|
14
22
|
|
|
15
23
|
DEFAULT_COLUMNS = 80
|
|
16
24
|
|
|
@@ -103,7 +111,7 @@ class Context(typ.NamedTuple):
|
|
|
103
111
|
max_context_len: int
|
|
104
112
|
|
|
105
113
|
|
|
106
|
-
def _iter_entry_paths(entries:
|
|
114
|
+
def _iter_entry_paths(entries: StackFrameEntryList) -> typ.Iterable[str]:
|
|
107
115
|
for entry in entries:
|
|
108
116
|
module_abspath = os.path.abspath(entry.module)
|
|
109
117
|
is_valid_abspath = module_abspath != entry.module and os.path.exists(
|
|
@@ -184,7 +192,7 @@ def _iter_alias_prefixes(entry_paths: typ.List[str]) -> typ.Iterable[AliasPrefix
|
|
|
184
192
|
|
|
185
193
|
|
|
186
194
|
def _iter_entry_rows(
|
|
187
|
-
aliases: AliasPrefixes, entry_paths: typ.List[str], entries:
|
|
195
|
+
aliases: AliasPrefixes, entry_paths: typ.List[str], entries: StackFrameEntryList
|
|
188
196
|
) -> typ.Iterable[Row]:
|
|
189
197
|
for abs_module, entry in zip(entry_paths, entries):
|
|
190
198
|
used_alias = ""
|
|
@@ -219,7 +227,7 @@ def _iter_entry_rows(
|
|
|
219
227
|
|
|
220
228
|
|
|
221
229
|
def _init_entries_context(
|
|
222
|
-
entries:
|
|
230
|
+
entries: StackFrameEntryList, term_width: typ.Optional[int] = None
|
|
223
231
|
) -> Context:
|
|
224
232
|
if term_width is None:
|
|
225
233
|
_term_width = _get_terminal_width()
|
|
@@ -361,19 +369,21 @@ def _rows_to_lines(
|
|
|
361
369
|
yield line
|
|
362
370
|
|
|
363
371
|
|
|
364
|
-
def _traceback_to_entries(traceback: types.TracebackType) ->
|
|
372
|
+
def _traceback_to_entries(traceback: types.TracebackType) -> StackFrameEntryList:
|
|
365
373
|
summary = tb.extract_tb(traceback)
|
|
374
|
+
entries = []
|
|
366
375
|
for entry in summary:
|
|
367
376
|
module = entry[0]
|
|
368
377
|
call = entry[2]
|
|
369
378
|
lineno = str(entry[1])
|
|
370
379
|
context = entry[3] or ""
|
|
371
|
-
|
|
380
|
+
entries.append(StackFrameEntry(module, call, lineno, context))
|
|
381
|
+
return entries
|
|
372
382
|
|
|
373
383
|
|
|
374
384
|
def _format_traceback(
|
|
375
385
|
ctx: Context,
|
|
376
|
-
traceback:
|
|
386
|
+
traceback: ExceptionTraceback,
|
|
377
387
|
color: bool = False,
|
|
378
388
|
local_stack_only: bool = False,
|
|
379
389
|
) -> str:
|
|
@@ -381,10 +391,10 @@ def _format_traceback(
|
|
|
381
391
|
|
|
382
392
|
lines = []
|
|
383
393
|
if ctx.aliases and not ctx.is_wide_mode:
|
|
384
|
-
lines.append(
|
|
394
|
+
lines.append(ALIASES_HEAD)
|
|
385
395
|
lines.extend(_aliases_to_lines(ctx, color))
|
|
386
396
|
|
|
387
|
-
lines.append(
|
|
397
|
+
lines.append(TRACEBACK_HEAD)
|
|
388
398
|
lines.extend(_rows_to_lines(padded_rows, color, local_stack_only))
|
|
389
399
|
|
|
390
400
|
if traceback.exc_name == "RecursionError" and len(lines) > 100:
|
|
@@ -416,14 +426,14 @@ def _format_traceback(
|
|
|
416
426
|
|
|
417
427
|
|
|
418
428
|
def format_traceback(
|
|
419
|
-
traceback:
|
|
429
|
+
traceback: ExceptionTraceback, color: bool = False, local_stack_only: bool = False
|
|
420
430
|
) -> str:
|
|
421
|
-
ctx = _init_entries_context(traceback.
|
|
431
|
+
ctx = _init_entries_context(traceback.stack_frames)
|
|
422
432
|
return _format_traceback(ctx, traceback, color, local_stack_only)
|
|
423
433
|
|
|
424
434
|
|
|
425
435
|
def format_tracebacks(
|
|
426
|
-
tracebacks: typ.List[
|
|
436
|
+
tracebacks: typ.List[ExceptionTraceback],
|
|
427
437
|
color: bool = False,
|
|
428
438
|
local_stack_only: bool = False,
|
|
429
439
|
) -> str:
|
|
@@ -432,10 +442,10 @@ def format_tracebacks(
|
|
|
432
442
|
for tb_tup in tracebacks:
|
|
433
443
|
if tb_tup.is_caused:
|
|
434
444
|
# traceback_strs.append("vvv caused by ^^^ - ")
|
|
435
|
-
traceback_strs.append(
|
|
445
|
+
traceback_strs.append(CAUSE_HEAD + os.linesep)
|
|
436
446
|
elif tb_tup.is_context:
|
|
437
447
|
# traceback_strs.append("vvv happend after ^^^ - ")
|
|
438
|
-
traceback_strs.append(
|
|
448
|
+
traceback_strs.append(CONTEXT_HEAD + os.linesep)
|
|
439
449
|
|
|
440
450
|
traceback_str = format_traceback(tb_tup, color, local_stack_only)
|
|
441
451
|
traceback_strs.append(traceback_str)
|
|
@@ -452,11 +462,12 @@ def exc_to_traceback_str(
|
|
|
452
462
|
traceback: types.TracebackType,
|
|
453
463
|
color: bool = False,
|
|
454
464
|
local_stack_only: bool = False,
|
|
465
|
+
exc_msg_override: str | None = None,
|
|
455
466
|
) -> str:
|
|
456
467
|
# NOTE (mb 2020-08-13): wrt. cause vs context see
|
|
457
468
|
# https://www.python.org/dev/peps/pep-3134/#enhanced-reporting
|
|
458
469
|
# https://stackoverflow.com/questions/11235932/
|
|
459
|
-
tracebacks: typ.List[
|
|
470
|
+
tracebacks: typ.List[ExceptionTraceback] = []
|
|
460
471
|
|
|
461
472
|
cur_exc_value: BaseException = exc_value
|
|
462
473
|
cur_traceback: types.TracebackType = traceback
|
|
@@ -475,10 +486,14 @@ def exc_to_traceback_str(
|
|
|
475
486
|
next_cause = getattr(cur_exc_value, "__cause__", None)
|
|
476
487
|
next_context = getattr(cur_exc_value, "__context__", None)
|
|
477
488
|
|
|
478
|
-
|
|
489
|
+
exc_msg = str(cur_exc_value)
|
|
490
|
+
if exc_msg_override is not None and cur_exc_value is exc_value:
|
|
491
|
+
exc_msg = exc_msg_override
|
|
492
|
+
|
|
493
|
+
tb_tup = ExceptionTraceback(
|
|
479
494
|
exc_name=type(cur_exc_value).__name__,
|
|
480
|
-
exc_msg=
|
|
481
|
-
|
|
495
|
+
exc_msg=exc_msg,
|
|
496
|
+
stack_frames=_traceback_to_entries(cur_traceback),
|
|
482
497
|
is_caused=bool(next_cause),
|
|
483
498
|
is_context=bool(next_context),
|
|
484
499
|
)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""JSON exception formatting for production logging.
|
|
2
|
+
|
|
3
|
+
This module provides exc_to_json() which converts exceptions to structured
|
|
4
|
+
dictionaries suitable for JSON logging in production environments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import types
|
|
8
|
+
import typing as typ
|
|
9
|
+
|
|
10
|
+
from beautiful_traceback.common import ExceptionTraceback, ExceptionTracebackList
|
|
11
|
+
import beautiful_traceback.formatting as fmt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _row_to_json_frame(row: fmt.Row) -> dict[str, typ.Any]:
|
|
15
|
+
"""Convert a Row to a JSON-serializable frame dict."""
|
|
16
|
+
return {
|
|
17
|
+
"module": row.short_module,
|
|
18
|
+
"alias": row.alias,
|
|
19
|
+
"function": row.call,
|
|
20
|
+
"lineno": int(row.lineno),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_traceback_json(
|
|
25
|
+
rows: list[fmt.Row],
|
|
26
|
+
exc_name: str,
|
|
27
|
+
exc_msg: str,
|
|
28
|
+
local_stack_only: bool,
|
|
29
|
+
) -> dict[str, typ.Any]:
|
|
30
|
+
"""Convert rows to a JSON-serializable traceback dict."""
|
|
31
|
+
if local_stack_only:
|
|
32
|
+
filtered_rows = [row for row in rows if row.alias == "<pwd>"]
|
|
33
|
+
else:
|
|
34
|
+
filtered_rows = rows
|
|
35
|
+
|
|
36
|
+
frames = [_row_to_json_frame(row) for row in filtered_rows]
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"exception": exc_name,
|
|
40
|
+
"message": exc_msg,
|
|
41
|
+
"frames": frames,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def exc_to_json(
|
|
46
|
+
exc_value: BaseException,
|
|
47
|
+
traceback: types.TracebackType | None,
|
|
48
|
+
local_stack_only: bool = False,
|
|
49
|
+
) -> dict[str, typ.Any]:
|
|
50
|
+
"""Convert an exception to a JSON-serializable dictionary.
|
|
51
|
+
|
|
52
|
+
This function is designed for production JSON logging. It always uses
|
|
53
|
+
aliased paths (e.g., <pwd>/app.py, <site>/requests/sessions.py) and
|
|
54
|
+
respects the local_stack_only flag to filter library frames.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
exc_value: The exception instance
|
|
58
|
+
traceback: The traceback object (can be None)
|
|
59
|
+
local_stack_only: If True, only include frames from <pwd> (current directory)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A dictionary with exception details and stack frames, including any
|
|
63
|
+
chained exceptions. Structure:
|
|
64
|
+
{
|
|
65
|
+
"exception": "ExceptionName",
|
|
66
|
+
"message": "exception message",
|
|
67
|
+
"frames": [
|
|
68
|
+
{
|
|
69
|
+
"module": "app.py",
|
|
70
|
+
"alias": "<pwd>",
|
|
71
|
+
"function": "function_name",
|
|
72
|
+
"lineno": 42
|
|
73
|
+
},
|
|
74
|
+
...
|
|
75
|
+
],
|
|
76
|
+
"chain": [ # optional, if exception has __cause__ or __context__
|
|
77
|
+
{
|
|
78
|
+
"exception": "CauseName",
|
|
79
|
+
"message": "cause message",
|
|
80
|
+
"relationship": "caused_by", # or "context"
|
|
81
|
+
"frames": [...]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> import sys
|
|
88
|
+
>>> try:
|
|
89
|
+
... raise ValueError("test error")
|
|
90
|
+
... except ValueError:
|
|
91
|
+
... exc_info = sys.exc_info()
|
|
92
|
+
... result = exc_to_json(exc_info[1], exc_info[2])
|
|
93
|
+
... print(result["exception"])
|
|
94
|
+
ValueError
|
|
95
|
+
"""
|
|
96
|
+
tracebacks = _exc_to_traceback_list(exc_value, traceback)
|
|
97
|
+
|
|
98
|
+
if not tracebacks:
|
|
99
|
+
return {
|
|
100
|
+
"exception": type(exc_value).__name__,
|
|
101
|
+
"message": str(exc_value),
|
|
102
|
+
"frames": [],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main_tb = tracebacks[0]
|
|
106
|
+
entries = list(main_tb.stack_frames)
|
|
107
|
+
|
|
108
|
+
if not entries:
|
|
109
|
+
result = {
|
|
110
|
+
"exception": main_tb.exc_name,
|
|
111
|
+
"message": main_tb.exc_msg,
|
|
112
|
+
"frames": [],
|
|
113
|
+
}
|
|
114
|
+
else:
|
|
115
|
+
ctx = fmt._init_entries_context(entries, term_width=fmt.DEFAULT_COLUMNS)
|
|
116
|
+
result = _format_traceback_json(
|
|
117
|
+
ctx.rows,
|
|
118
|
+
main_tb.exc_name,
|
|
119
|
+
main_tb.exc_msg,
|
|
120
|
+
local_stack_only,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if len(tracebacks) > 1:
|
|
124
|
+
chain = []
|
|
125
|
+
for tb in tracebacks[1:]:
|
|
126
|
+
entries = list(tb.stack_frames)
|
|
127
|
+
if not entries:
|
|
128
|
+
chain_item = {
|
|
129
|
+
"exception": tb.exc_name,
|
|
130
|
+
"message": tb.exc_msg,
|
|
131
|
+
"relationship": "caused_by" if tb.is_caused else "context",
|
|
132
|
+
"frames": [],
|
|
133
|
+
}
|
|
134
|
+
else:
|
|
135
|
+
ctx = fmt._init_entries_context(entries, term_width=fmt.DEFAULT_COLUMNS)
|
|
136
|
+
chain_item = _format_traceback_json(
|
|
137
|
+
ctx.rows,
|
|
138
|
+
tb.exc_name,
|
|
139
|
+
tb.exc_msg,
|
|
140
|
+
local_stack_only,
|
|
141
|
+
)
|
|
142
|
+
chain_item["relationship"] = "caused_by" if tb.is_caused else "context"
|
|
143
|
+
chain.append(chain_item)
|
|
144
|
+
|
|
145
|
+
result["chain"] = chain
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _exc_to_traceback_list(
|
|
151
|
+
exc_value: BaseException,
|
|
152
|
+
traceback: types.TracebackType | None,
|
|
153
|
+
) -> ExceptionTracebackList:
|
|
154
|
+
"""Convert exception with chaining to a list of Traceback objects.
|
|
155
|
+
|
|
156
|
+
Handles __cause__ and __context__ chains, detecting circular references.
|
|
157
|
+
"""
|
|
158
|
+
tracebacks: ExceptionTracebackList = []
|
|
159
|
+
seen_exceptions: set[int] = set()
|
|
160
|
+
|
|
161
|
+
current_exc = exc_value
|
|
162
|
+
current_tb = traceback
|
|
163
|
+
is_caused = False
|
|
164
|
+
is_context = False
|
|
165
|
+
|
|
166
|
+
while current_exc is not None:
|
|
167
|
+
exc_id = id(current_exc)
|
|
168
|
+
|
|
169
|
+
if exc_id in seen_exceptions:
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
seen_exceptions.add(exc_id)
|
|
173
|
+
|
|
174
|
+
entries = fmt._traceback_to_entries(current_tb) if current_tb else []
|
|
175
|
+
|
|
176
|
+
tb_obj = ExceptionTraceback(
|
|
177
|
+
exc_name=type(current_exc).__name__,
|
|
178
|
+
exc_msg=str(current_exc),
|
|
179
|
+
stack_frames=entries,
|
|
180
|
+
is_caused=is_caused,
|
|
181
|
+
is_context=is_context,
|
|
182
|
+
)
|
|
183
|
+
tracebacks.append(tb_obj)
|
|
184
|
+
|
|
185
|
+
next_exc = None
|
|
186
|
+
next_tb = None
|
|
187
|
+
|
|
188
|
+
if current_exc.__cause__ is not None:
|
|
189
|
+
next_exc = current_exc.__cause__
|
|
190
|
+
next_tb = next_exc.__traceback__
|
|
191
|
+
is_caused = True
|
|
192
|
+
is_context = False
|
|
193
|
+
elif (
|
|
194
|
+
current_exc.__context__ is not None and not current_exc.__suppress_context__
|
|
195
|
+
):
|
|
196
|
+
next_exc = current_exc.__context__
|
|
197
|
+
next_tb = next_exc.__traceback__
|
|
198
|
+
is_caused = False
|
|
199
|
+
is_context = True
|
|
200
|
+
|
|
201
|
+
current_exc = next_exc
|
|
202
|
+
current_tb = next_tb
|
|
203
|
+
|
|
204
|
+
return tracebacks
|
beautiful_traceback/parsing.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import typing as typ
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
from beautiful_traceback.common import (
|
|
5
|
+
CAUSE_HEAD,
|
|
6
|
+
CONTEXT_HEAD,
|
|
7
|
+
TRACEBACK_HEAD,
|
|
8
|
+
ExceptionTraceback,
|
|
9
|
+
ExceptionTracebackList,
|
|
10
|
+
StackFrameEntry,
|
|
11
|
+
)
|
|
5
12
|
|
|
6
13
|
# TODO (mb 2020-08-12): path/module with doublequotes in them.
|
|
7
14
|
# Not even sure what python does with that.
|
|
@@ -20,7 +27,7 @@ File
|
|
|
20
27
|
LOCATION_RE = re.compile(LOCATION_PATTERN, flags=re.VERBOSE)
|
|
21
28
|
|
|
22
29
|
|
|
23
|
-
def _parse_entries(entry_lines: typ.List[str]) -> typ.Iterable[
|
|
30
|
+
def _parse_entries(entry_lines: typ.List[str]) -> typ.Iterable[StackFrameEntry]:
|
|
24
31
|
i = 0
|
|
25
32
|
while i < len(entry_lines):
|
|
26
33
|
line = entry_lines[i]
|
|
@@ -43,13 +50,13 @@ def _parse_entries(entry_lines: typ.List[str]) -> typ.Iterable[com.Entry]:
|
|
|
43
50
|
|
|
44
51
|
module, lineno, call = loc_match.groups()
|
|
45
52
|
|
|
46
|
-
yield
|
|
53
|
+
yield StackFrameEntry(module, call, lineno, src_ctx)
|
|
47
54
|
|
|
48
55
|
|
|
49
|
-
TRACE_HEADERS = {
|
|
56
|
+
TRACE_HEADERS = {TRACEBACK_HEAD, CAUSE_HEAD, CONTEXT_HEAD}
|
|
50
57
|
|
|
51
58
|
|
|
52
|
-
def _iter_tracebacks(trace: str) -> typ.Iterable[
|
|
59
|
+
def _iter_tracebacks(trace: str) -> typ.Iterable[ExceptionTraceback]:
|
|
53
60
|
lines = trace.strip().splitlines()
|
|
54
61
|
|
|
55
62
|
i = 0
|
|
@@ -64,17 +71,17 @@ def _iter_tracebacks(trace: str) -> typ.Iterable[com.Traceback]:
|
|
|
64
71
|
is_caused = False
|
|
65
72
|
is_context = False
|
|
66
73
|
|
|
67
|
-
if line.startswith(
|
|
74
|
+
if line.startswith(CAUSE_HEAD):
|
|
68
75
|
is_caused = True
|
|
69
76
|
i += 1
|
|
70
|
-
elif line.startswith(
|
|
77
|
+
elif line.startswith(CONTEXT_HEAD):
|
|
71
78
|
is_context = True
|
|
72
79
|
i += 1
|
|
73
80
|
|
|
74
81
|
# skip empty lines and tb head
|
|
75
82
|
while i < len(lines):
|
|
76
83
|
line = lines[i].strip()
|
|
77
|
-
if not line or line.startswith(
|
|
84
|
+
if not line or line.startswith(TRACEBACK_HEAD):
|
|
78
85
|
i += 1
|
|
79
86
|
else:
|
|
80
87
|
break
|
|
@@ -93,10 +100,10 @@ def _iter_tracebacks(trace: str) -> typ.Iterable[com.Traceback]:
|
|
|
93
100
|
exc_msg = ""
|
|
94
101
|
|
|
95
102
|
entries = list(_parse_entries(entry_lines))
|
|
96
|
-
yield
|
|
103
|
+
yield ExceptionTraceback(
|
|
97
104
|
exc_name=exc_name,
|
|
98
105
|
exc_msg=exc_msg,
|
|
99
|
-
|
|
106
|
+
stack_frames=entries,
|
|
100
107
|
is_caused=is_caused,
|
|
101
108
|
is_context=is_context,
|
|
102
109
|
)
|
|
@@ -104,7 +111,7 @@ def _iter_tracebacks(trace: str) -> typ.Iterable[com.Traceback]:
|
|
|
104
111
|
i += 1
|
|
105
112
|
|
|
106
113
|
|
|
107
|
-
def parse_tracebacks(trace: str) ->
|
|
114
|
+
def parse_tracebacks(trace: str) -> ExceptionTracebackList:
|
|
108
115
|
"""Parses a chain of tracebacks.
|
|
109
116
|
|
|
110
117
|
Args:
|
|
@@ -19,6 +19,43 @@ def _get_option(config: Config, key: str):
|
|
|
19
19
|
return val
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def _get_exception_message_override(excinfo: pytest.ExceptionInfo) -> str | None:
|
|
23
|
+
"""Return pytest's verbose exception message when rewriting adds detail.
|
|
24
|
+
|
|
25
|
+
The plugin overrides pytest's longrepr rendering, which skips the
|
|
26
|
+
ExceptionInfo repr where pytest stores rewritten assertion diffs. Without
|
|
27
|
+
this, AssertionError messages collapse to str(exc) and omit left/right
|
|
28
|
+
details. Pulling reprcrash.message preserves that verbose message when
|
|
29
|
+
pytest provides one.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
repr_info = excinfo.getrepr(style="long")
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
reprcrash = getattr(repr_info, "reprcrash", None)
|
|
37
|
+
if reprcrash is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
message = getattr(reprcrash, "message", None)
|
|
41
|
+
if not message:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
exc_name = type(excinfo.value).__name__
|
|
45
|
+
prefix = f"{exc_name}: "
|
|
46
|
+
if message.startswith(prefix):
|
|
47
|
+
message = message.removeprefix(prefix)
|
|
48
|
+
|
|
49
|
+
if not message or message == exc_name:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
exc_message = str(excinfo.value)
|
|
53
|
+
if message == exc_message:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
return message
|
|
57
|
+
|
|
58
|
+
|
|
22
59
|
def pytest_addoption(parser):
|
|
23
60
|
parser.addini(
|
|
24
61
|
"enable_beautiful_traceback",
|
|
@@ -52,6 +89,8 @@ def pytest_runtest_makereport(item, call):
|
|
|
52
89
|
value = call.excinfo.value
|
|
53
90
|
tb = call.excinfo.tb
|
|
54
91
|
|
|
92
|
+
message_override = _get_exception_message_override(call.excinfo)
|
|
93
|
+
|
|
55
94
|
formatted_traceback = formatting.exc_to_traceback_str(
|
|
56
95
|
value,
|
|
57
96
|
tb,
|
|
@@ -59,6 +98,7 @@ def pytest_runtest_makereport(item, call):
|
|
|
59
98
|
local_stack_only=_get_option(
|
|
60
99
|
item.config, "enable_beautiful_traceback_local_stack_only"
|
|
61
100
|
),
|
|
101
|
+
exc_msg_override=message_override,
|
|
62
102
|
)
|
|
63
103
|
report.longrepr = formatted_traceback
|
|
64
104
|
|
|
@@ -72,6 +112,8 @@ def pytest_exception_interact(node, call, report):
|
|
|
72
112
|
if report.failed:
|
|
73
113
|
value = call.excinfo.value
|
|
74
114
|
tb = call.excinfo.tb
|
|
115
|
+
message_override = _get_exception_message_override(call.excinfo)
|
|
116
|
+
|
|
75
117
|
formatted_traceback = formatting.exc_to_traceback_str(
|
|
76
118
|
value,
|
|
77
119
|
tb,
|
|
@@ -79,5 +121,6 @@ def pytest_exception_interact(node, call, report):
|
|
|
79
121
|
local_stack_only=_get_option(
|
|
80
122
|
node.config, "enable_beautiful_traceback_local_stack_only"
|
|
81
123
|
),
|
|
124
|
+
exc_msg_override=message_override,
|
|
82
125
|
)
|
|
83
126
|
report.longrepr = formatted_traceback
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
beautiful_traceback/__init__.py,sha256=e6zDdmcyc3H2aAmY5_9hAeMtCU_b3xyXwGhqrbOa-qo,438
|
|
2
|
+
beautiful_traceback/_extension.py,sha256=klyo3XL4q3-Wdy4Lt6JYdh-Cfh_SkRCI7jCcQCwfWTM,311
|
|
3
|
+
beautiful_traceback/cli.py,sha256=M4EWW9SUNiGH2VCpssxRDMNXNjrVpy_8t9T7jf66RKE,2405
|
|
4
|
+
beautiful_traceback/common.py,sha256=Dg6J4rLdX9uKM6LxJaqSwtLkUjggZk-KrR5kpAIA4uo,2208
|
|
5
|
+
beautiful_traceback/formatting.py,sha256=s4tjdejhYBXBmHlHiUa3bm9j0I6B2hka46JfHNdgKLk,15501
|
|
6
|
+
beautiful_traceback/hook.py,sha256=6vYpqA-mD4G32HkX5WSyCMzkvMwB4RXP6wbVEIU6oaY,2078
|
|
7
|
+
beautiful_traceback/json_formatting.py,sha256=WcgA6rk6YkFDRLXOTk95yVrK_HxNjvHHUqgr2RlK_O4,6071
|
|
8
|
+
beautiful_traceback/parsing.py,sha256=39GvHo6kx5dyzTMfRjUTc8H9tRGIhFT8RcjxUvbZBZY,3094
|
|
9
|
+
beautiful_traceback/pytest_plugin.py,sha256=IwZh5yUUsbLpUWZuVxVqSIGUVPO30_0xIHThc3xor54,3672
|
|
10
|
+
beautiful_traceback-0.3.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
11
|
+
beautiful_traceback-0.3.0.dist-info/entry_points.txt,sha256=EXsu7N89wqDpZPEwLHgYVncZJ3y-lbCFZC5fKBZZDac,138
|
|
12
|
+
beautiful_traceback-0.3.0.dist-info/METADATA,sha256=Z5pu8bStps0bEkS8f53AJ5I7krXVHpu2WVdibKPco_s,8558
|
|
13
|
+
beautiful_traceback-0.3.0.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
beautiful_traceback/__init__.py,sha256=onuaATNEzmniuSLwhI7ec-3enL8XaQ2wmXS5o2l4qxc,462
|
|
2
|
-
beautiful_traceback/_extension.py,sha256=klyo3XL4q3-Wdy4Lt6JYdh-Cfh_SkRCI7jCcQCwfWTM,311
|
|
3
|
-
beautiful_traceback/cli.py,sha256=rcoEkeBQvipWHhVTPgd5yRfNNzSDmxNaReP8K2o9eYM,2030
|
|
4
|
-
beautiful_traceback/common.py,sha256=IIg46wUk5e0syCrkI2HfLPwv8zi8mQ5AIPgb1998lAY,585
|
|
5
|
-
beautiful_traceback/formatting.py,sha256=nN1wv2scW0tJw2VJyisqlaaT-MM_mOuo2sY5w_cdoTM,15093
|
|
6
|
-
beautiful_traceback/hook.py,sha256=6vYpqA-mD4G32HkX5WSyCMzkvMwB4RXP6wbVEIU6oaY,2078
|
|
7
|
-
beautiful_traceback/parsing.py,sha256=lVqOty6X9MqTfo-lTIQjI_zVxUqRhYEVjyGhkW8wps8,2954
|
|
8
|
-
beautiful_traceback/pytest_plugin.py,sha256=vdRgvycWeG9wwh9tK4921ugW9aHx0Zib1Or7mU9Khw0,2315
|
|
9
|
-
beautiful_traceback-0.2.0.dist-info/WHEEL,sha256=Pi5uDq5Fdo_Rr-HD5h9BiPn9Et29Y9Sh8NhcJNnFU1c,79
|
|
10
|
-
beautiful_traceback-0.2.0.dist-info/entry_points.txt,sha256=EXsu7N89wqDpZPEwLHgYVncZJ3y-lbCFZC5fKBZZDac,138
|
|
11
|
-
beautiful_traceback-0.2.0.dist-info/METADATA,sha256=UNpYywFmNYvVgjrYOj8DD-6hkIcVX58XVnTrRCbvrNk,8558
|
|
12
|
-
beautiful_traceback-0.2.0.dist-info/RECORD,,
|
{beautiful_traceback-0.2.0.dist-info → beautiful_traceback-0.3.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|