beautiful-traceback 0.1.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.
@@ -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
- __version__ = "0.1.0"
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
  ]
@@ -0,0 +1,84 @@
1
+ """CLI commands for beautiful-traceback installation and configuration."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import colorama
7
+
8
+
9
+ def inject_pth() -> None:
10
+ """
11
+ Inject beautiful-traceback into the current Python environment via .pth file.
12
+
13
+ Creates a .pth file in site-packages that automatically imports beautiful_traceback
14
+ on interpreter startup. Only works within virtual environments.
15
+ """
16
+ if not _is_in_venv():
17
+ print("Error: Not running in a virtual environment", file=sys.stderr)
18
+ print(
19
+ "Beautiful traceback pth injection only works in virtual environments",
20
+ file=sys.stderr,
21
+ )
22
+ sys.exit(1)
23
+
24
+ site_packages = _get_site_packages()
25
+ pth_file = site_packages / "beautiful_traceback_injection.pth"
26
+ py_file = site_packages / "_beautiful_traceback_injection.py"
27
+
28
+ _create_injection_files(py_file, pth_file)
29
+
30
+ print(f"Beautiful traceback injection installed: {pth_file}")
31
+
32
+
33
+ def _is_in_venv() -> bool:
34
+ """Check if running in a virtual environment."""
35
+ return hasattr(sys, "real_prefix") or (
36
+ hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
37
+ )
38
+
39
+
40
+ def _get_site_packages() -> Path:
41
+ """Get the site-packages directory for the current Python environment."""
42
+ import site
43
+
44
+ site_packages_list = site.getsitepackages()
45
+
46
+ if not site_packages_list:
47
+ print("Error: Could not find site-packages directory", file=sys.stderr)
48
+ sys.exit(1)
49
+
50
+ return Path(site_packages_list[0])
51
+
52
+
53
+ def _create_injection_files(py_file: Path, pth_file: Path) -> None:
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
+
64
+ py_content = """def run_startup_script():
65
+ try:
66
+ import beautiful_traceback
67
+ beautiful_traceback.install(only_tty=False)
68
+ except ImportError:
69
+ pass
70
+
71
+ run_startup_script()
72
+ """
73
+
74
+ py_file.write_text(py_content)
75
+ pth_file.write_text("import _beautiful_traceback_injection\n")
76
+
77
+
78
+ def main() -> None:
79
+ """Main CLI entrypoint."""
80
+ inject_pth()
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -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 Entry(typ.NamedTuple):
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
- Entries = typ.List[Entry]
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
- entries: Entries
51
+ stack_frames: StackFrameEntryList
18
52
 
19
53
  is_caused: bool
20
54
  is_context: bool
21
55
 
22
56
 
23
- Tracebacks = typ.List[Traceback]
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
- import beautiful_traceback.common as com
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: com.Entries) -> typ.Iterable[str]:
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: com.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: com.Entries, term_width: typ.Optional[int] = None
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) -> typ.Iterable[com.Entry]:
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
- yield com.Entry(module, call, lineno, context)
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: com.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(com.ALIASES_HEAD)
394
+ lines.append(ALIASES_HEAD)
385
395
  lines.extend(_aliases_to_lines(ctx, color))
386
396
 
387
- lines.append(com.TRACEBACK_HEAD)
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: com.Traceback, color: bool = False, local_stack_only: bool = False
429
+ traceback: ExceptionTraceback, color: bool = False, local_stack_only: bool = False
420
430
  ) -> str:
421
- ctx = _init_entries_context(traceback.entries)
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[com.Traceback],
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(com.CAUSE_HEAD + os.linesep)
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(com.CONTEXT_HEAD + os.linesep)
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,15 +462,16 @@ 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[com.Traceback] = []
470
+ tracebacks: typ.List[ExceptionTraceback] = []
460
471
 
461
472
  cur_exc_value: BaseException = exc_value
462
473
  cur_traceback: types.TracebackType = traceback
463
-
474
+
464
475
  # Track seen exceptions to prevent infinite loops from circular references
465
476
  seen_exceptions: typ.Set[int] = set()
466
477
 
@@ -471,14 +482,18 @@ def exc_to_traceback_str(
471
482
  # Circular reference detected, break the loop
472
483
  break
473
484
  seen_exceptions.add(exc_id)
474
-
485
+
475
486
  next_cause = getattr(cur_exc_value, "__cause__", None)
476
487
  next_context = getattr(cur_exc_value, "__context__", None)
477
488
 
478
- tb_tup = com.Traceback(
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=str(cur_exc_value),
481
- entries=list(_traceback_to_entries(cur_traceback)),
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
@@ -1,7 +1,14 @@
1
1
  import re
2
2
  import typing as typ
3
3
 
4
- import beautiful_traceback.common as com
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[com.Entry]:
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 com.Entry(module, call, lineno, src_ctx)
53
+ yield StackFrameEntry(module, call, lineno, src_ctx)
47
54
 
48
55
 
49
- TRACE_HEADERS = {com.TRACEBACK_HEAD, com.CAUSE_HEAD, com.CONTEXT_HEAD}
56
+ TRACE_HEADERS = {TRACEBACK_HEAD, CAUSE_HEAD, CONTEXT_HEAD}
50
57
 
51
58
 
52
- def _iter_tracebacks(trace: str) -> typ.Iterable[com.Traceback]:
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(com.CAUSE_HEAD):
74
+ if line.startswith(CAUSE_HEAD):
68
75
  is_caused = True
69
76
  i += 1
70
- elif line.startswith(com.CONTEXT_HEAD):
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(com.TRACEBACK_HEAD):
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 com.Traceback(
103
+ yield ExceptionTraceback(
97
104
  exc_name=exc_name,
98
105
  exc_msg=exc_msg,
99
- entries=entries,
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) -> com.Tracebacks:
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: beautiful-traceback
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Beautiful, readable Python tracebacks with colors and formatting
5
5
  Keywords: traceback,error,debugging,formatting
6
6
  Author: Michael Bianco
@@ -10,14 +10,17 @@ Requires-Python: >=3.9
10
10
  Project-URL: Repository, https://github.com/iloveitaly/beautiful-traceback
11
11
  Description-Content-Type: text/markdown
12
12
 
13
- # Beautiful Traceback
13
+ # Beautiful, Readable Python Stack Traces
14
14
 
15
- > **Note:** This is a fork of the [pretty-traceback](https://github.com/mbarkhau/pretty-traceback) repo with simplified development and improvements for better integration with FastAPI, [structlog](https://github.com/iloveitaly/structlog-config), IPython, pytest, and more. This project is used in [python-starter-template](https://github.com/iloveitaly/python-starter-template) to provide better debugging experience in production environments.
15
+ [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
16
+ [![Python Versions](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
16
17
 
17
18
  Human readable stacktraces for Python.
18
19
 
19
- [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
20
- [![Python Versions](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
20
+ ![Comparison of standard Python traceback vs Beautiful Traceback](comparison.webp)
21
+
22
+ > [!NOTE]
23
+ > This is a fork of the [pretty-traceback](https://github.com/mbarkhau/pretty-traceback) repo with simplified development and improvements for better integration with FastAPI, [structlog](https://github.com/iloveitaly/structlog-config), IPython, pytest, and more. This project is used in [python-starter-template](https://github.com/iloveitaly/python-starter-template) to provide better debugging experience in production environments.
21
24
 
22
25
  ## Quick Start
23
26
 
@@ -34,7 +37,6 @@ uv run examples/simple.py
34
37
 
35
38
  Beautiful Traceback groups together what belongs together, adds coloring and alignment. All of this makes it easier for you to see patterns and filter out the signal from the noise. This tabular format is best viewed in a wide terminal.
36
39
 
37
- ![Comparison of standard Python traceback vs Beautiful Traceback](comparison.webp)
38
40
 
39
41
  ## Installation
40
42
 
@@ -202,7 +204,27 @@ This gives you full control over the log format while adding beautiful traceback
202
204
 
203
205
  You can enable beautiful-traceback across all Python projects without modifying any source code by using a `.pth` file. Python automatically executes import statements in `.pth` files during interpreter startup, making this perfect for development environments.
204
206
 
205
- Add this function to your `.zshrc` or `.bashrc`:
207
+ ### Using the CLI Command
208
+
209
+ The easiest way to inject beautiful-traceback into your current virtual environment:
210
+
211
+ ```bash
212
+ beautiful-traceback
213
+ ```
214
+
215
+ This command:
216
+ - Only works within virtual environments (for safety)
217
+ - Installs the `.pth` file into your current environment's site-packages
218
+ - Displays the installation path every time it runs
219
+
220
+ Output:
221
+ ```
222
+ Beautiful traceback injection installed: /path/to/.venv/lib/python3.11/site-packages/beautiful_traceback_injection.pth
223
+ ```
224
+
225
+ ### Using a Shell Function (Alternative)
226
+
227
+ Alternatively, add this function to your `.zshrc` or `.bashrc`:
206
228
 
207
229
  ```bash
208
230
  # Create a file to automatically import beautiful-traceback on startup
@@ -216,7 +238,7 @@ python-inject-beautiful-traceback() {
216
238
  def run_startup_script():
217
239
  try:
218
240
  import beautiful_traceback
219
- beautiful_traceback.install()
241
+ beautiful_traceback.install(only_tty=False)
220
242
  except ImportError:
221
243
  pass
222
244
 
@@ -249,4 +271,4 @@ Beautiful Traceback is heavily inspired by the backtrace module by [nir0s](https
249
271
 
250
272
  ## License
251
273
 
252
- MIT License - see [LICENSE.md](LICENSE.md) for details.
274
+ [MIT License](LICENSE.md)
@@ -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,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.17
2
+ Generator: uv 0.9.26
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ beautiful-traceback = beautiful_traceback.cli:main
3
+
4
+ [pytest11]
5
+ beautiful_traceback = beautiful_traceback.pytest_plugin
6
+
@@ -1,11 +0,0 @@
1
- beautiful_traceback/__init__.py,sha256=XW0PVUTOPa6qFDhvE8oPWikOcL8Xbfgv_BLuFdkVhCI,462
2
- beautiful_traceback/_extension.py,sha256=klyo3XL4q3-Wdy4Lt6JYdh-Cfh_SkRCI7jCcQCwfWTM,311
3
- beautiful_traceback/common.py,sha256=IIg46wUk5e0syCrkI2HfLPwv8zi8mQ5AIPgb1998lAY,585
4
- beautiful_traceback/formatting.py,sha256=FilxU_holFyBvVKd2VHTtqQ4sRfUlfxSIYwHLtadR3M,15105
5
- beautiful_traceback/hook.py,sha256=6vYpqA-mD4G32HkX5WSyCMzkvMwB4RXP6wbVEIU6oaY,2078
6
- beautiful_traceback/parsing.py,sha256=lVqOty6X9MqTfo-lTIQjI_zVxUqRhYEVjyGhkW8wps8,2954
7
- beautiful_traceback/pytest_plugin.py,sha256=vdRgvycWeG9wwh9tK4921ugW9aHx0Zib1Or7mU9Khw0,2315
8
- beautiful_traceback-0.1.0.dist-info/WHEEL,sha256=Pi5uDq5Fdo_Rr-HD5h9BiPn9Et29Y9Sh8NhcJNnFU1c,79
9
- beautiful_traceback-0.1.0.dist-info/entry_points.txt,sha256=m99Hs6ia_rvyRu-ruwKwVCjob50f-wvLVtztWyi47a8,68
10
- beautiful_traceback-0.1.0.dist-info/METADATA,sha256=w0nVy_HVr836OK_I1jY1fosVLAXM_3MOgm5992CvmQc,8020
11
- beautiful_traceback-0.1.0.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- [pytest11]
2
- beautiful_traceback = beautiful_traceback.pytest_plugin
3
-