beautiful-traceback 0.3.0__tar.gz → 0.4.0__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: beautiful-traceback
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Beautiful, readable Python tracebacks with colors and formatting
5
5
  Keywords: traceback,error,debugging,formatting
6
6
  Author: Michael Bianco
@@ -0,0 +1,7 @@
1
+ from ._extension import load_ipython_extension # noqa: F401
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
+
6
+ # retain typo for backward compatibility
7
+ LoggingFormaterMixin = LoggingFormatterMixin
@@ -0,0 +1,169 @@
1
+ import os
2
+ from typing import Any, Generator
3
+
4
+ from . import formatting
5
+ import pytest
6
+
7
+ from pytest import Config
8
+
9
+
10
+ def _get_option(config: Config, key: str) -> Any:
11
+ val = None
12
+
13
+ # will throw an exception if option is not set
14
+ try:
15
+ val = config.getoption(key)
16
+ except Exception:
17
+ pass
18
+
19
+ if val is None:
20
+ val = config.getini(key)
21
+
22
+ return val
23
+
24
+
25
+ def _get_exception_message_override(excinfo: pytest.ExceptionInfo) -> str | None:
26
+ """Return pytest's verbose exception message when rewriting adds detail.
27
+
28
+ The plugin overrides pytest's longrepr rendering, which skips the
29
+ ExceptionInfo repr where pytest stores rewritten assertion diffs. Without
30
+ this, AssertionError messages collapse to str(exc) and omit left/right
31
+ details. Pulling reprcrash.message preserves that verbose message when
32
+ pytest provides one.
33
+ """
34
+ try:
35
+ repr_info = excinfo.getrepr(style="long")
36
+ except Exception:
37
+ return None
38
+
39
+ reprcrash = getattr(repr_info, "reprcrash", None)
40
+ if reprcrash is None:
41
+ return None
42
+
43
+ message = getattr(reprcrash, "message", None)
44
+ if not message:
45
+ return None
46
+
47
+ exc_name = type(excinfo.value).__name__
48
+ prefix = f"{exc_name}: "
49
+ if message.startswith(prefix):
50
+ message = message.removeprefix(prefix)
51
+
52
+ if not message or message == exc_name:
53
+ return None
54
+
55
+ exc_message = str(excinfo.value)
56
+ if message == exc_message:
57
+ return None
58
+
59
+ return message
60
+
61
+
62
+ def _get_pytest_assertion_details(excinfo: pytest.ExceptionInfo) -> str | None:
63
+ """Return the pytest assertion diff lines for AssertionError."""
64
+ if not isinstance(excinfo.value, AssertionError):
65
+ return None
66
+
67
+ try:
68
+ # pytest stores assertion diffs on its own repr object, not the exception.
69
+ # Reference: https://github.com/pytest-dev/pytest/blob/main/src/_pytest/_code/code.py
70
+ repr_info = excinfo.getrepr(style="long")
71
+ except Exception:
72
+ return None
73
+
74
+ reprtraceback = getattr(repr_info, "reprtraceback", None)
75
+ if reprtraceback is None:
76
+ chain = getattr(repr_info, "chain", None)
77
+ if chain:
78
+ reprtraceback = chain[-1][0]
79
+
80
+ if reprtraceback is None:
81
+ return None
82
+
83
+ reprentries = getattr(reprtraceback, "reprentries", None)
84
+ if not reprentries:
85
+ return None
86
+
87
+ last_entry = reprentries[-1]
88
+ entry_lines = getattr(last_entry, "lines", None)
89
+ if not entry_lines:
90
+ return None
91
+
92
+ # Keep only the assertion diff lines for concise appending.
93
+ lines = []
94
+ for line in entry_lines:
95
+ # Keep pytest's assertion diff lines and the failing expression.
96
+ stripped = line.lstrip()
97
+ if stripped.startswith("E"):
98
+ lines.append(stripped)
99
+ continue
100
+
101
+ # Include the source line marker when present.
102
+ if stripped.startswith(">"):
103
+ lines.append(stripped)
104
+
105
+ if not lines:
106
+ return None
107
+
108
+ return os.linesep.join(lines)
109
+
110
+
111
+ def _format_traceback(excinfo: pytest.ExceptionInfo, config: Config) -> str:
112
+ """Format a traceback with beautiful_traceback styling and pytest assertion details."""
113
+ message_override = _get_exception_message_override(excinfo)
114
+ assertion_details = _get_pytest_assertion_details(excinfo)
115
+
116
+ formatted_traceback = formatting.exc_to_traceback_str(
117
+ excinfo.value,
118
+ excinfo.tb,
119
+ color=True,
120
+ local_stack_only=_get_option(
121
+ config, "enable_beautiful_traceback_local_stack_only"
122
+ ),
123
+ exc_msg_override=message_override,
124
+ )
125
+
126
+ if assertion_details:
127
+ formatted_traceback += os.linesep + assertion_details + os.linesep
128
+
129
+ return formatted_traceback
130
+
131
+
132
+ def pytest_addoption(parser) -> None:
133
+ parser.addini(
134
+ "enable_beautiful_traceback",
135
+ "Enable the beautiful traceback plugin",
136
+ type="bool",
137
+ default=True,
138
+ )
139
+
140
+ parser.addini(
141
+ "enable_beautiful_traceback_local_stack_only",
142
+ "Show only local code (filter out library/framework internals)",
143
+ type="bool",
144
+ default=True,
145
+ )
146
+
147
+
148
+ @pytest.hookimpl(hookwrapper=True)
149
+ def pytest_runtest_makereport(item, call) -> Generator[None, None, None]:
150
+ """Format test execution tracebacks with beautiful_traceback.
151
+
152
+ This hook runs during the test execution phase and replaces pytest's
153
+ default traceback formatting with beautiful_traceback's output.
154
+ """
155
+ outcome = yield # type: ignore[misc]
156
+ report = outcome.get_result() # type: ignore[attr-defined]
157
+
158
+ if _get_option(item.config, "enable_beautiful_traceback") and report.failed:
159
+ report.longrepr = _format_traceback(call.excinfo, item.config)
160
+
161
+
162
+ def pytest_exception_interact(node, call, report) -> None:
163
+ """Format collection-phase tracebacks with beautiful_traceback.
164
+
165
+ This hook runs during collection (e.g., import errors, fixture errors)
166
+ and ensures those errors also use beautiful_traceback formatting.
167
+ """
168
+ if _get_option(node.config, "enable_beautiful_traceback") and report.failed:
169
+ report.longrepr = _format_traceback(call.excinfo, node.config)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "beautiful-traceback"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Beautiful, readable Python tracebacks with colors and formatting"
5
5
  keywords = ["traceback", "error", "debugging", "formatting"]
6
6
  readme = "README.md"
@@ -1,17 +0,0 @@
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
5
-
6
- # retain typo for backward compatibility
7
- LoggingFormaterMixin = LoggingFormatterMixin
8
-
9
-
10
- __all__ = [
11
- "install",
12
- "uninstall",
13
- "LoggingFormatter",
14
- "LoggingFormatterMixin",
15
- "LoggingFormaterMixin",
16
- "exc_to_json",
17
- ]
@@ -1,126 +0,0 @@
1
- from . import formatting
2
- import pytest
3
-
4
- from pytest import Config
5
-
6
-
7
- def _get_option(config: Config, key: str):
8
- val = None
9
-
10
- # will throw an exception if option is not set
11
- try:
12
- val = config.getoption(key)
13
- except Exception:
14
- pass
15
-
16
- if val is None:
17
- val = config.getini(key)
18
-
19
- return val
20
-
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
-
59
- def pytest_addoption(parser):
60
- parser.addini(
61
- "enable_beautiful_traceback",
62
- "Enable the beautiful traceback plugin",
63
- type="bool",
64
- default=True,
65
- )
66
-
67
- parser.addini(
68
- "enable_beautiful_traceback_local_stack_only",
69
- "Show only local code (filter out library/framework internals)",
70
- type="bool",
71
- default=True,
72
- )
73
-
74
-
75
- @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:
81
-
82
- https://grok.com/share/bGVnYWN5_951be3b1-6811-4fda-b220-c1dd72dedc31
83
- """
84
- outcome = yield
85
- report = outcome.get_result() # Get the generated TestReport object
86
-
87
- # Check if the report is for the 'call' phase (test execution) and if it failed
88
- if _get_option(item.config, "enable_beautiful_traceback") and report.failed:
89
- value = call.excinfo.value
90
- tb = call.excinfo.tb
91
-
92
- message_override = _get_exception_message_override(call.excinfo)
93
-
94
- formatted_traceback = formatting.exc_to_traceback_str(
95
- value,
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.
109
-
110
- So, if there's an import or other pre-run error in pytest, this will apply the correct formatting.
111
- """
112
- if report.failed:
113
- value = call.excinfo.value
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