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.
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/PKG-INFO +1 -1
- beautiful_traceback-0.4.0/beautiful_traceback/__init__.py +7 -0
- beautiful_traceback-0.4.0/beautiful_traceback/pytest_plugin.py +169 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/pyproject.toml +1 -1
- beautiful_traceback-0.3.0/beautiful_traceback/__init__.py +0 -17
- beautiful_traceback-0.3.0/beautiful_traceback/pytest_plugin.py +0 -126
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/README.md +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/_extension.py +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/cli.py +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/common.py +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/formatting.py +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/hook.py +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/json_formatting.py +0 -0
- {beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/parsing.py +0 -0
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beautiful_traceback-0.3.0 → beautiful_traceback-0.4.0}/beautiful_traceback/json_formatting.py
RENAMED
|
File without changes
|
|
File without changes
|