beautiful-traceback 0.3.0__py3-none-any.whl → 0.5.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 +3 -13
- beautiful_traceback/formatting.py +78 -5
- beautiful_traceback/hook.py +16 -3
- beautiful_traceback/json_formatting.py +11 -2
- beautiful_traceback/pytest_assertion.py +94 -0
- beautiful_traceback/pytest_plugin.py +57 -80
- {beautiful_traceback-0.3.0.dist-info → beautiful_traceback-0.5.0.dist-info}/METADATA +26 -1
- beautiful_traceback-0.5.0.dist-info/RECORD +14 -0
- beautiful_traceback-0.3.0.dist-info/RECORD +0 -13
- {beautiful_traceback-0.3.0.dist-info → beautiful_traceback-0.5.0.dist-info}/WHEEL +0 -0
- {beautiful_traceback-0.3.0.dist-info → beautiful_traceback-0.5.0.dist-info}/entry_points.txt +0 -0
beautiful_traceback/__init__.py
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
from ._extension import load_ipython_extension # noqa: F401
|
|
2
|
-
from .formatting import LoggingFormatter, LoggingFormatterMixin
|
|
3
|
-
from .hook import install, uninstall
|
|
4
|
-
from .json_formatting import exc_to_json
|
|
2
|
+
from .formatting import LoggingFormatter, LoggingFormatterMixin # noqa: F401
|
|
3
|
+
from .hook import install, uninstall # noqa: F401
|
|
4
|
+
from .json_formatting import exc_to_json # noqa: F401
|
|
5
5
|
|
|
6
6
|
# retain typo for backward compatibility
|
|
7
7
|
LoggingFormaterMixin = LoggingFormatterMixin
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
__all__ = [
|
|
11
|
-
"install",
|
|
12
|
-
"uninstall",
|
|
13
|
-
"LoggingFormatter",
|
|
14
|
-
"LoggingFormatterMixin",
|
|
15
|
-
"LoggingFormaterMixin",
|
|
16
|
-
"exc_to_json",
|
|
17
|
-
]
|
|
@@ -129,6 +129,46 @@ TEST_PATHS: typ.List[str] = []
|
|
|
129
129
|
PWD = os.getcwd()
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
def _compile_exclude_patterns(
|
|
133
|
+
exclude_patterns: typ.Sequence[str],
|
|
134
|
+
) -> list[re.Pattern[str]]:
|
|
135
|
+
if not exclude_patterns:
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
return [re.compile(pattern) for pattern in exclude_patterns]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _row_matches_exclude_patterns(
|
|
142
|
+
row: Row, exclude_patterns: typ.Sequence[re.Pattern[str]]
|
|
143
|
+
) -> bool:
|
|
144
|
+
if not exclude_patterns:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# Try multiple representations so patterns can match different parts:
|
|
148
|
+
# 1. short_module: "_pytest/runner.py" (aliased path without prefix)
|
|
149
|
+
# 2. full_module: "/path/to/site-packages/_pytest/runner.py" (absolute path)
|
|
150
|
+
# 3. haystack_full: "<site> /path/to/site-packages/_pytest/runner.py:353 from_call result: ..."
|
|
151
|
+
# 4. haystack_short: "<site> _pytest/runner.py:353 from_call result: ..."
|
|
152
|
+
haystack_full = (
|
|
153
|
+
f"{row.alias} {row.full_module}:{row.lineno} {row.call} {row.context}"
|
|
154
|
+
)
|
|
155
|
+
haystack_short = (
|
|
156
|
+
f"{row.alias} {row.short_module}:{row.lineno} {row.call} {row.context}"
|
|
157
|
+
)
|
|
158
|
+
candidates = [
|
|
159
|
+
row.short_module,
|
|
160
|
+
row.full_module,
|
|
161
|
+
haystack_full,
|
|
162
|
+
haystack_short,
|
|
163
|
+
]
|
|
164
|
+
for pattern in exclude_patterns:
|
|
165
|
+
for candidate in candidates:
|
|
166
|
+
if pattern.search(candidate):
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
132
172
|
def _py_paths() -> typ.List[str]:
|
|
133
173
|
if TEST_PATHS:
|
|
134
174
|
return TEST_PATHS
|
|
@@ -227,7 +267,9 @@ def _iter_entry_rows(
|
|
|
227
267
|
|
|
228
268
|
|
|
229
269
|
def _init_entries_context(
|
|
230
|
-
entries: StackFrameEntryList,
|
|
270
|
+
entries: StackFrameEntryList,
|
|
271
|
+
term_width: typ.Optional[int] = None,
|
|
272
|
+
exclude_patterns: typ.Sequence[str] = (),
|
|
231
273
|
) -> Context:
|
|
232
274
|
if term_width is None:
|
|
233
275
|
_term_width = _get_terminal_width()
|
|
@@ -246,6 +288,19 @@ def _init_entries_context(
|
|
|
246
288
|
max_row_width = _term_width - 10
|
|
247
289
|
|
|
248
290
|
rows = list(_iter_entry_rows(aliases, entry_paths, entries))
|
|
291
|
+
compiled_exclude_patterns = _compile_exclude_patterns(exclude_patterns)
|
|
292
|
+
if compiled_exclude_patterns:
|
|
293
|
+
rows = [
|
|
294
|
+
row
|
|
295
|
+
for row in rows
|
|
296
|
+
if not _row_matches_exclude_patterns(row, compiled_exclude_patterns)
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
used_aliases = {row.alias for row in rows if row.alias}
|
|
300
|
+
if used_aliases:
|
|
301
|
+
aliases = [alias for alias in aliases if alias[0] in used_aliases]
|
|
302
|
+
else:
|
|
303
|
+
aliases = []
|
|
249
304
|
|
|
250
305
|
if rows:
|
|
251
306
|
max_short_module_len = max(
|
|
@@ -426,9 +481,15 @@ def _format_traceback(
|
|
|
426
481
|
|
|
427
482
|
|
|
428
483
|
def format_traceback(
|
|
429
|
-
traceback: ExceptionTraceback,
|
|
484
|
+
traceback: ExceptionTraceback,
|
|
485
|
+
color: bool = False,
|
|
486
|
+
local_stack_only: bool = False,
|
|
487
|
+
exclude_patterns: typ.Sequence[str] = (),
|
|
430
488
|
) -> str:
|
|
431
|
-
ctx = _init_entries_context(
|
|
489
|
+
ctx = _init_entries_context(
|
|
490
|
+
traceback.stack_frames,
|
|
491
|
+
exclude_patterns=exclude_patterns,
|
|
492
|
+
)
|
|
432
493
|
return _format_traceback(ctx, traceback, color, local_stack_only)
|
|
433
494
|
|
|
434
495
|
|
|
@@ -436,6 +497,7 @@ def format_tracebacks(
|
|
|
436
497
|
tracebacks: typ.List[ExceptionTraceback],
|
|
437
498
|
color: bool = False,
|
|
438
499
|
local_stack_only: bool = False,
|
|
500
|
+
exclude_patterns: typ.Sequence[str] = (),
|
|
439
501
|
) -> str:
|
|
440
502
|
traceback_strs: typ.List[str] = []
|
|
441
503
|
|
|
@@ -447,7 +509,12 @@ def format_tracebacks(
|
|
|
447
509
|
# traceback_strs.append("vvv happend after ^^^ - ")
|
|
448
510
|
traceback_strs.append(CONTEXT_HEAD + os.linesep)
|
|
449
511
|
|
|
450
|
-
traceback_str = format_traceback(
|
|
512
|
+
traceback_str = format_traceback(
|
|
513
|
+
tb_tup,
|
|
514
|
+
color,
|
|
515
|
+
local_stack_only,
|
|
516
|
+
exclude_patterns=exclude_patterns,
|
|
517
|
+
)
|
|
451
518
|
traceback_strs.append(traceback_str)
|
|
452
519
|
|
|
453
520
|
return os.linesep.join(traceback_strs).strip()
|
|
@@ -463,6 +530,7 @@ def exc_to_traceback_str(
|
|
|
463
530
|
color: bool = False,
|
|
464
531
|
local_stack_only: bool = False,
|
|
465
532
|
exc_msg_override: str | None = None,
|
|
533
|
+
exclude_patterns: typ.Sequence[str] = (),
|
|
466
534
|
) -> str:
|
|
467
535
|
# NOTE (mb 2020-08-13): wrt. cause vs context see
|
|
468
536
|
# https://www.python.org/dev/peps/pep-3134/#enhanced-reporting
|
|
@@ -511,7 +579,12 @@ def exc_to_traceback_str(
|
|
|
511
579
|
|
|
512
580
|
tracebacks = list(reversed(tracebacks))
|
|
513
581
|
|
|
514
|
-
return format_tracebacks(
|
|
582
|
+
return format_tracebacks(
|
|
583
|
+
tracebacks,
|
|
584
|
+
color,
|
|
585
|
+
local_stack_only,
|
|
586
|
+
exclude_patterns=exclude_patterns,
|
|
587
|
+
)
|
|
515
588
|
|
|
516
589
|
|
|
517
590
|
class LoggingFormatterMixin:
|
beautiful_traceback/hook.py
CHANGED
|
@@ -8,7 +8,11 @@ import colorama
|
|
|
8
8
|
from beautiful_traceback import formatting
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def init_excepthook(
|
|
11
|
+
def init_excepthook(
|
|
12
|
+
color: bool,
|
|
13
|
+
local_stack_only: bool,
|
|
14
|
+
exclude_patterns: typ.Sequence[str],
|
|
15
|
+
) -> typ.Callable:
|
|
12
16
|
def excepthook(
|
|
13
17
|
exc_type: typ.Type[BaseException],
|
|
14
18
|
exc_value: BaseException,
|
|
@@ -17,7 +21,11 @@ def init_excepthook(color: bool, local_stack_only: bool) -> typ.Callable:
|
|
|
17
21
|
# pylint:disable=unused-argument
|
|
18
22
|
tb_str = (
|
|
19
23
|
formatting.exc_to_traceback_str(
|
|
20
|
-
exc_value,
|
|
24
|
+
exc_value,
|
|
25
|
+
traceback,
|
|
26
|
+
color,
|
|
27
|
+
local_stack_only,
|
|
28
|
+
exclude_patterns=exclude_patterns,
|
|
21
29
|
)
|
|
22
30
|
+ "\n"
|
|
23
31
|
)
|
|
@@ -39,6 +47,7 @@ def install(
|
|
|
39
47
|
only_tty: bool = True,
|
|
40
48
|
only_hook_if_default_excepthook: bool = True,
|
|
41
49
|
local_stack_only: bool = False,
|
|
50
|
+
exclude_patterns: typ.Sequence[str] = (),
|
|
42
51
|
) -> None:
|
|
43
52
|
"""Hook the current excepthook to the beautiful_traceback.
|
|
44
53
|
|
|
@@ -68,7 +77,11 @@ def install(
|
|
|
68
77
|
if only_hook_if_default_excepthook and not is_default_exepthook:
|
|
69
78
|
return
|
|
70
79
|
|
|
71
|
-
sys.excepthook = init_excepthook(
|
|
80
|
+
sys.excepthook = init_excepthook(
|
|
81
|
+
color=color,
|
|
82
|
+
local_stack_only=local_stack_only,
|
|
83
|
+
exclude_patterns=exclude_patterns,
|
|
84
|
+
)
|
|
72
85
|
|
|
73
86
|
|
|
74
87
|
def uninstall() -> None:
|
|
@@ -46,6 +46,7 @@ def exc_to_json(
|
|
|
46
46
|
exc_value: BaseException,
|
|
47
47
|
traceback: types.TracebackType | None,
|
|
48
48
|
local_stack_only: bool = False,
|
|
49
|
+
exclude_patterns: typ.Sequence[str] = (),
|
|
49
50
|
) -> dict[str, typ.Any]:
|
|
50
51
|
"""Convert an exception to a JSON-serializable dictionary.
|
|
51
52
|
|
|
@@ -112,7 +113,11 @@ def exc_to_json(
|
|
|
112
113
|
"frames": [],
|
|
113
114
|
}
|
|
114
115
|
else:
|
|
115
|
-
ctx = fmt._init_entries_context(
|
|
116
|
+
ctx = fmt._init_entries_context(
|
|
117
|
+
entries,
|
|
118
|
+
term_width=fmt.DEFAULT_COLUMNS,
|
|
119
|
+
exclude_patterns=exclude_patterns,
|
|
120
|
+
)
|
|
116
121
|
result = _format_traceback_json(
|
|
117
122
|
ctx.rows,
|
|
118
123
|
main_tb.exc_name,
|
|
@@ -132,7 +137,11 @@ def exc_to_json(
|
|
|
132
137
|
"frames": [],
|
|
133
138
|
}
|
|
134
139
|
else:
|
|
135
|
-
ctx = fmt._init_entries_context(
|
|
140
|
+
ctx = fmt._init_entries_context(
|
|
141
|
+
entries,
|
|
142
|
+
term_width=fmt.DEFAULT_COLUMNS,
|
|
143
|
+
exclude_patterns=exclude_patterns,
|
|
144
|
+
)
|
|
136
145
|
chain_item = _format_traceback_json(
|
|
137
146
|
ctx.rows,
|
|
138
147
|
tb.exc_name,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Helpers for extracting pytest's rewritten assertion details."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_exception_message_override(excinfo: pytest.ExceptionInfo) -> str | None:
|
|
9
|
+
"""Return pytest's verbose exception message when rewriting adds detail.
|
|
10
|
+
|
|
11
|
+
The plugin overrides pytest's longrepr rendering, which skips the
|
|
12
|
+
ExceptionInfo repr where pytest stores rewritten assertion diffs. Without
|
|
13
|
+
this, AssertionError messages collapse to str(exc) and omit left/right
|
|
14
|
+
details. Pulling reprcrash.message preserves that verbose message when
|
|
15
|
+
pytest provides one.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
repr_info = excinfo.getrepr(style="long")
|
|
19
|
+
except Exception:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
reprcrash = getattr(repr_info, "reprcrash", None)
|
|
23
|
+
if reprcrash is None:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
message = getattr(reprcrash, "message", None)
|
|
27
|
+
if not message:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
exc_name = type(excinfo.value).__name__
|
|
31
|
+
prefix = f"{exc_name}: "
|
|
32
|
+
if message.startswith(prefix):
|
|
33
|
+
message = message.removeprefix(prefix)
|
|
34
|
+
|
|
35
|
+
if not message or message == exc_name:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
exc_message = str(excinfo.value)
|
|
39
|
+
if message == exc_message:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
return message
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_pytest_assertion_details(excinfo: pytest.ExceptionInfo) -> str | None:
|
|
46
|
+
"""Return pytest's rewritten assertion lines for AssertionError.
|
|
47
|
+
|
|
48
|
+
Pytest only provides left/right diffs when a module is rewritten during
|
|
49
|
+
import. For helper modules, call pytest.register_assert_rewrite before
|
|
50
|
+
importing them, otherwise the assertion message is plain and there are no
|
|
51
|
+
diff lines to extract.
|
|
52
|
+
"""
|
|
53
|
+
if not isinstance(excinfo.value, AssertionError):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# pytest stores assertion diffs on its own repr object, not the exception.
|
|
58
|
+
# Reference: https://github.com/pytest-dev/pytest/blob/main/src/_pytest/_code/code.py
|
|
59
|
+
repr_info = excinfo.getrepr(style="long")
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
reprtraceback = getattr(repr_info, "reprtraceback", None)
|
|
64
|
+
if reprtraceback is None:
|
|
65
|
+
chain = getattr(repr_info, "chain", None)
|
|
66
|
+
if chain:
|
|
67
|
+
reprtraceback = chain[-1][0]
|
|
68
|
+
|
|
69
|
+
if reprtraceback is None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
reprentries = getattr(reprtraceback, "reprentries", None)
|
|
73
|
+
if not reprentries:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
last_entry = reprentries[-1]
|
|
77
|
+
entry_lines = getattr(last_entry, "lines", None)
|
|
78
|
+
if not entry_lines:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
lines = []
|
|
82
|
+
for line in entry_lines:
|
|
83
|
+
stripped = line.lstrip()
|
|
84
|
+
if stripped.startswith("E"):
|
|
85
|
+
lines.append(stripped)
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if stripped.startswith(">"):
|
|
89
|
+
lines.append(stripped)
|
|
90
|
+
|
|
91
|
+
if not lines:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
return os.linesep.join(lines)
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""Pytest plugin that preserves rewritten assertion details in tracebacks.
|
|
2
|
+
|
|
3
|
+
Pytest rewrites assert statements at import time for test modules (and any
|
|
4
|
+
modules registered via pytest.register_assert_rewrite). The rewritten asserts
|
|
5
|
+
raise AssertionError instances that include rich explanation text and left/right
|
|
6
|
+
diffs inside pytest's repr objects, not on the exception itself. This plugin
|
|
7
|
+
extracts those repr details and appends them to beautiful_traceback output.
|
|
8
|
+
"""
|
|
3
9
|
|
|
10
|
+
import os
|
|
11
|
+
from typing import Any, Generator
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
4
14
|
from pytest import Config
|
|
5
15
|
|
|
16
|
+
from . import formatting
|
|
17
|
+
from .pytest_assertion import get_exception_message_override
|
|
18
|
+
from .pytest_assertion import get_pytest_assertion_details
|
|
19
|
+
|
|
6
20
|
|
|
7
|
-
def _get_option(config: Config, key: str):
|
|
21
|
+
def _get_option(config: Config, key: str) -> Any:
|
|
8
22
|
val = None
|
|
9
23
|
|
|
10
24
|
# will throw an exception if option is not set
|
|
@@ -19,44 +33,30 @@ def _get_option(config: Config, key: str):
|
|
|
19
33
|
return val
|
|
20
34
|
|
|
21
35
|
|
|
22
|
-
def
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
36
|
+
def _format_traceback(excinfo: pytest.ExceptionInfo, config: Config) -> str:
|
|
37
|
+
"""Format a traceback with beautiful_traceback styling and pytest details."""
|
|
38
|
+
message_override = get_exception_message_override(excinfo)
|
|
39
|
+
assertion_details = get_pytest_assertion_details(excinfo)
|
|
40
|
+
exclude_patterns = _get_option(config, "beautiful_traceback_exclude_patterns")
|
|
41
|
+
|
|
42
|
+
formatted_traceback = formatting.exc_to_traceback_str(
|
|
43
|
+
excinfo.value,
|
|
44
|
+
excinfo.tb,
|
|
45
|
+
color=True,
|
|
46
|
+
local_stack_only=_get_option(
|
|
47
|
+
config, "enable_beautiful_traceback_local_stack_only"
|
|
48
|
+
),
|
|
49
|
+
exc_msg_override=message_override,
|
|
50
|
+
exclude_patterns=exclude_patterns,
|
|
51
|
+
)
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return None
|
|
53
|
+
if assertion_details:
|
|
54
|
+
formatted_traceback += os.linesep + assertion_details + os.linesep
|
|
55
55
|
|
|
56
|
-
return
|
|
56
|
+
return formatted_traceback
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def pytest_addoption(parser):
|
|
59
|
+
def pytest_addoption(parser) -> None:
|
|
60
60
|
parser.addini(
|
|
61
61
|
"enable_beautiful_traceback",
|
|
62
62
|
"Enable the beautiful traceback plugin",
|
|
@@ -71,56 +71,33 @@ def pytest_addoption(parser):
|
|
|
71
71
|
default=True,
|
|
72
72
|
)
|
|
73
73
|
|
|
74
|
+
parser.addini(
|
|
75
|
+
"beautiful_traceback_exclude_patterns",
|
|
76
|
+
"Exclude traceback frames that match regex patterns",
|
|
77
|
+
type="linelist",
|
|
78
|
+
default=[],
|
|
79
|
+
)
|
|
80
|
+
|
|
74
81
|
|
|
75
82
|
@pytest.hookimpl(hookwrapper=True)
|
|
76
|
-
def pytest_runtest_makereport(item, call):
|
|
77
|
-
"""
|
|
78
|
-
Pytest stack traces are challenging to work with by default. This plugin allows beautiful_traceback to be used instead.
|
|
79
|
-
|
|
80
|
-
This little piece of code was hard-won:
|
|
83
|
+
def pytest_runtest_makereport(item, call) -> Generator[None, None, None]:
|
|
84
|
+
"""Format test execution tracebacks with beautiful_traceback.
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
This hook runs during the test execution phase and replaces pytest's
|
|
87
|
+
default traceback formatting with beautiful_traceback's output.
|
|
83
88
|
"""
|
|
84
|
-
outcome = yield
|
|
85
|
-
report = outcome.get_result() #
|
|
89
|
+
outcome = yield # type: ignore[misc]
|
|
90
|
+
report = outcome.get_result() # type: ignore[attr-defined]
|
|
86
91
|
|
|
87
|
-
# Check if the report is for the 'call' phase (test execution) and if it failed
|
|
88
92
|
if _get_option(item.config, "enable_beautiful_traceback") and report.failed:
|
|
89
|
-
|
|
90
|
-
tb = call.excinfo.tb
|
|
93
|
+
report.longrepr = _format_traceback(call.excinfo, item.config)
|
|
91
94
|
|
|
92
|
-
message_override = _get_exception_message_override(call.excinfo)
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
tb,
|
|
97
|
-
color=True,
|
|
98
|
-
local_stack_only=_get_option(
|
|
99
|
-
item.config, "enable_beautiful_traceback_local_stack_only"
|
|
100
|
-
),
|
|
101
|
-
exc_msg_override=message_override,
|
|
102
|
-
)
|
|
103
|
-
report.longrepr = formatted_traceback
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def pytest_exception_interact(node, call, report):
|
|
107
|
-
"""
|
|
108
|
-
This can run during collection, not just test execution.
|
|
96
|
+
def pytest_exception_interact(node, call, report) -> None:
|
|
97
|
+
"""Format collection-phase tracebacks with beautiful_traceback.
|
|
109
98
|
|
|
110
|
-
|
|
99
|
+
This hook runs during collection (e.g., import errors, fixture errors)
|
|
100
|
+
and ensures those errors also use beautiful_traceback formatting.
|
|
111
101
|
"""
|
|
112
|
-
if report.failed:
|
|
113
|
-
|
|
114
|
-
tb = call.excinfo.tb
|
|
115
|
-
message_override = _get_exception_message_override(call.excinfo)
|
|
116
|
-
|
|
117
|
-
formatted_traceback = formatting.exc_to_traceback_str(
|
|
118
|
-
value,
|
|
119
|
-
tb,
|
|
120
|
-
color=True,
|
|
121
|
-
local_stack_only=_get_option(
|
|
122
|
-
node.config, "enable_beautiful_traceback_local_stack_only"
|
|
123
|
-
),
|
|
124
|
-
exc_msg_override=message_override,
|
|
125
|
-
)
|
|
126
|
-
report.longrepr = formatted_traceback
|
|
102
|
+
if _get_option(node.config, "enable_beautiful_traceback") and report.failed:
|
|
103
|
+
report.longrepr = _format_traceback(call.excinfo, node.config)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: beautiful-traceback
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Beautiful, readable Python tracebacks with colors and formatting
|
|
5
5
|
Keywords: traceback,error,debugging,formatting
|
|
6
6
|
Author: Michael Bianco
|
|
@@ -142,6 +142,9 @@ Customize the plugin in your `pytest.ini` or `pyproject.toml`:
|
|
|
142
142
|
[tool.pytest.ini_options]
|
|
143
143
|
enable_beautiful_traceback = true # Enable/disable the plugin
|
|
144
144
|
enable_beautiful_traceback_local_stack_only = true # Show only local code (filter libraries)
|
|
145
|
+
beautiful_traceback_exclude_patterns = [ # Regex patterns to drop frames
|
|
146
|
+
"click/core\\.py",
|
|
147
|
+
]
|
|
145
148
|
```
|
|
146
149
|
|
|
147
150
|
Or in `pytest.ini`:
|
|
@@ -150,8 +153,29 @@ Or in `pytest.ini`:
|
|
|
150
153
|
[pytest]
|
|
151
154
|
enable_beautiful_traceback = true
|
|
152
155
|
enable_beautiful_traceback_local_stack_only = true
|
|
156
|
+
beautiful_traceback_exclude_patterns =
|
|
157
|
+
click/core\.py
|
|
153
158
|
```
|
|
154
159
|
|
|
160
|
+
Example: filter out pytest, pluggy, and playwright frames from CI tracebacks:
|
|
161
|
+
|
|
162
|
+
```toml
|
|
163
|
+
[tool.pytest.ini_options]
|
|
164
|
+
beautiful_traceback_exclude_patterns = [
|
|
165
|
+
"^_pytest/",
|
|
166
|
+
"^pluggy/",
|
|
167
|
+
"^playwright/",
|
|
168
|
+
]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Pattern Matching:** Patterns are tested against multiple representations of each frame:
|
|
172
|
+
- `_pytest/runner.py` (short module path)
|
|
173
|
+
- `/path/to/site-packages/_pytest/runner.py` (full module path)
|
|
174
|
+
- `<site> _pytest/runner.py:353 from_call result: ...` (formatted line with short path)
|
|
175
|
+
- `<site> /path/to/.../runner.py:353 from_call result: ...` (formatted line with full path)
|
|
176
|
+
|
|
177
|
+
This allows you to write simpler patterns like `^_pytest/` instead of needing to match the full site-packages path.
|
|
178
|
+
|
|
155
179
|
## Examples
|
|
156
180
|
|
|
157
181
|
Check out the [examples/](examples/) directory for detailed usage examples including basic usage, exception chaining, logging integration, and more.
|
|
@@ -176,6 +200,7 @@ beautiful_traceback.install(
|
|
|
176
200
|
only_tty=True, # Only activate for TTY output
|
|
177
201
|
only_hook_if_default_excepthook=True, # Only install if default hook
|
|
178
202
|
local_stack_only=False, # Filter to show only local code
|
|
203
|
+
exclude_patterns=["click/core\\.py"], # Regex patterns to drop frames
|
|
179
204
|
envvar='ENABLE_BEAUTIFUL_TRACEBACK' # Optional environment variable gate
|
|
180
205
|
)
|
|
181
206
|
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
beautiful_traceback/__init__.py,sha256=ZDE4AcfeeT420hCntDfdhnp4uhXm1YwCQHsTuvGc0L4,332
|
|
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=GI6F_l0KyHbMztCwq009oQ2uhAVRX-iDVUklODoQ5rc,17658
|
|
6
|
+
beautiful_traceback/hook.py,sha256=8f4njtVq8tlO8NXBcKV11paNmmfwYz13Nh6eWw6E9JE,2342
|
|
7
|
+
beautiful_traceback/json_formatting.py,sha256=qCGyOsHB4aAlxC2uSv2Lo-DfNAMf0EDeyN7AEAb9_Ts,6313
|
|
8
|
+
beautiful_traceback/parsing.py,sha256=39GvHo6kx5dyzTMfRjUTc8H9tRGIhFT8RcjxUvbZBZY,3094
|
|
9
|
+
beautiful_traceback/pytest_assertion.py,sha256=vyNTOsOzzR70T2oYvzytrvW0R4gCsO7pBkud8d85arw,2784
|
|
10
|
+
beautiful_traceback/pytest_plugin.py,sha256=aVKNoKfI4UmwXvRAV0dPTuYJuLk-rUYOZP_2y_pVteM,3395
|
|
11
|
+
beautiful_traceback-0.5.0.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
12
|
+
beautiful_traceback-0.5.0.dist-info/entry_points.txt,sha256=EXsu7N89wqDpZPEwLHgYVncZJ3y-lbCFZC5fKBZZDac,138
|
|
13
|
+
beautiful_traceback-0.5.0.dist-info/METADATA,sha256=BJYOONwKSbUybFFSo_QP8UgaAbjfSJyA6tmFmqnj4UE,9501
|
|
14
|
+
beautiful_traceback-0.5.0.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
{beautiful_traceback-0.3.0.dist-info → beautiful_traceback-0.5.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|