tracerite 2.2.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.
- tracerite/__init__.py +19 -0
- tracerite/fastapi.py +46 -0
- tracerite/html.py +343 -0
- tracerite/inspector.py +383 -0
- tracerite/logging.py +4 -0
- tracerite/notebook.py +81 -0
- tracerite/script.js +176 -0
- tracerite/style.css +304 -0
- tracerite/syntaxerror.py +417 -0
- tracerite/trace.py +918 -0
- tracerite/trace_cpy.py +408 -0
- tracerite/tty.py +793 -0
- tracerite-2.2.0.dist-info/METADATA +117 -0
- tracerite-2.2.0.dist-info/RECORD +15 -0
- tracerite-2.2.0.dist-info/WHEEL +4 -0
tracerite/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .fastapi import patch_fastapi
|
|
2
|
+
from .html import html_traceback
|
|
3
|
+
from .inspector import extract_variables, prettyvalue
|
|
4
|
+
from .notebook import load_ipython_extension, unload_ipython_extension
|
|
5
|
+
from .trace import extract_chain
|
|
6
|
+
from .tty import load, tty_traceback, unload
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"load",
|
|
10
|
+
"unload",
|
|
11
|
+
"tty_traceback",
|
|
12
|
+
"html_traceback",
|
|
13
|
+
"extract_chain",
|
|
14
|
+
"prettyvalue",
|
|
15
|
+
"extract_variables",
|
|
16
|
+
"load_ipython_extension",
|
|
17
|
+
"unload_ipython_extension",
|
|
18
|
+
"patch_fastapi",
|
|
19
|
+
]
|
tracerite/fastapi.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""TraceRite extension for FastAPI/Starlette applications."""
|
|
2
|
+
|
|
3
|
+
from .html import html_traceback
|
|
4
|
+
from .logging import logger
|
|
5
|
+
|
|
6
|
+
_original_debug_response = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def patch_fastapi():
|
|
10
|
+
"""
|
|
11
|
+
Load TraceRite extension for FastAPI by patching ServerErrorMiddleware.
|
|
12
|
+
|
|
13
|
+
This patches Starlette's ServerErrorMiddleware.debug_response to return
|
|
14
|
+
TraceRite HTML tracebacks instead of the default debug HTML when running
|
|
15
|
+
in debug mode and the client accepts HTML.
|
|
16
|
+
"""
|
|
17
|
+
global _original_debug_response
|
|
18
|
+
if _original_debug_response is not None:
|
|
19
|
+
return # Already loaded
|
|
20
|
+
try:
|
|
21
|
+
import fastapi # ty: ignore
|
|
22
|
+
from starlette.middleware.errors import ServerErrorMiddleware # ty: ignore
|
|
23
|
+
from starlette.responses import HTMLResponse # ty: ignore
|
|
24
|
+
except ImportError:
|
|
25
|
+
logger.info("TraceRite FastAPI cannot load: FastAPI/Starlette not found")
|
|
26
|
+
return
|
|
27
|
+
_original_debug_response = ServerErrorMiddleware.debug_response
|
|
28
|
+
|
|
29
|
+
def tracerite_debug_response(self, request, exc):
|
|
30
|
+
"""Return TraceRite HTML traceback instead of Starlette's debug response."""
|
|
31
|
+
accept = request.headers.get("accept", "")
|
|
32
|
+
if "text/html" not in accept:
|
|
33
|
+
return _original_debug_response(self, request, exc) # type: ignore
|
|
34
|
+
try:
|
|
35
|
+
html = str(html_traceback(exc=exc, include_js_css=True))
|
|
36
|
+
return HTMLResponse(
|
|
37
|
+
"<!DOCTYPE html><title>FastAPI TraceRite</title>" + html,
|
|
38
|
+
status_code=500,
|
|
39
|
+
)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.error(f"Failed to generate TraceRite response: {e}")
|
|
42
|
+
return _original_debug_response(self, request, exc) # type: ignore
|
|
43
|
+
|
|
44
|
+
ServerErrorMiddleware.debug_response = tracerite_debug_response # type: ignore
|
|
45
|
+
fastapi.routing.__tracebackhide__ = "until" # type: ignore
|
|
46
|
+
logger.info("TraceRite FastAPI extension loaded")
|
tracerite/html.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
from importlib.resources import files
|
|
2
|
+
|
|
3
|
+
from html5tagger import HTML, E
|
|
4
|
+
|
|
5
|
+
from .trace import chainmsg, extract_chain
|
|
6
|
+
|
|
7
|
+
style = files(__package__).joinpath("style.css").read_text(encoding="UTF-8")
|
|
8
|
+
javascript = files(__package__).joinpath("script.js").read_text(encoding="UTF-8")
|
|
9
|
+
|
|
10
|
+
detail_show = "{display: inherit}"
|
|
11
|
+
|
|
12
|
+
symbols = {"call": "➤", "warning": "⚠️", "error": "💣", "stop": "🛑"}
|
|
13
|
+
tooltips = {
|
|
14
|
+
"call": "Call",
|
|
15
|
+
"warning": "Call from your code",
|
|
16
|
+
"error": "{type}",
|
|
17
|
+
"stop": "{type}",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _collapse_call_runs(frames, min_run_length=10):
|
|
22
|
+
"""Collapse consecutive runs of 'call' frames, keeping first and last of each run.
|
|
23
|
+
|
|
24
|
+
Only collapses runs of frames with relevance='call'. Non-call frames
|
|
25
|
+
(error, warning, stop) are never collapsed.
|
|
26
|
+
"""
|
|
27
|
+
if not frames:
|
|
28
|
+
return frames
|
|
29
|
+
|
|
30
|
+
result = []
|
|
31
|
+
run_start = None
|
|
32
|
+
|
|
33
|
+
for i, frinfo in enumerate(frames):
|
|
34
|
+
if frinfo.get("relevance", "call") == "call":
|
|
35
|
+
if run_start is None:
|
|
36
|
+
run_start = i
|
|
37
|
+
else:
|
|
38
|
+
# End of a call run - process it
|
|
39
|
+
if run_start is not None:
|
|
40
|
+
run_length = i - run_start
|
|
41
|
+
if run_length >= min_run_length:
|
|
42
|
+
# Keep first and last of the run, add ellipsis
|
|
43
|
+
result.append(frames[run_start])
|
|
44
|
+
result.append(...)
|
|
45
|
+
result.append(frames[i - 1])
|
|
46
|
+
else:
|
|
47
|
+
# Run too short, keep all
|
|
48
|
+
result.extend(frames[run_start:i])
|
|
49
|
+
run_start = None
|
|
50
|
+
# Add the non-call frame
|
|
51
|
+
result.append(frinfo)
|
|
52
|
+
|
|
53
|
+
# Handle final run at end
|
|
54
|
+
if run_start is not None:
|
|
55
|
+
run_length = len(frames) - run_start
|
|
56
|
+
if run_length >= min_run_length:
|
|
57
|
+
result.append(frames[run_start])
|
|
58
|
+
result.append(...)
|
|
59
|
+
result.append(frames[-1])
|
|
60
|
+
else:
|
|
61
|
+
result.extend(frames[run_start:])
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def html_traceback(
|
|
67
|
+
exc=None,
|
|
68
|
+
chain=None,
|
|
69
|
+
*,
|
|
70
|
+
include_js_css=True,
|
|
71
|
+
local_urls=False,
|
|
72
|
+
replace_previous=False,
|
|
73
|
+
**extract_args,
|
|
74
|
+
):
|
|
75
|
+
chain = chain or extract_chain(exc=exc, **extract_args)[-3:]
|
|
76
|
+
# Chain is oldest-first from extract_chain
|
|
77
|
+
with E.div(
|
|
78
|
+
class_="tracerite", data_replace_previous="1" if replace_previous else None
|
|
79
|
+
) as doc:
|
|
80
|
+
if include_js_css:
|
|
81
|
+
doc._style(style)
|
|
82
|
+
for i, e in enumerate(chain):
|
|
83
|
+
# Get chaining suffix for exception header
|
|
84
|
+
chain_suffix = ""
|
|
85
|
+
if i > 0:
|
|
86
|
+
chain_suffix = chainmsg.get(e.get("from", "none"), "")
|
|
87
|
+
_exception(doc, e, local_urls=local_urls, chain_suffix=chain_suffix)
|
|
88
|
+
|
|
89
|
+
if include_js_css:
|
|
90
|
+
# Build scrollto calls
|
|
91
|
+
scrollto_calls = []
|
|
92
|
+
for e in reversed(chain):
|
|
93
|
+
for info in e["frames"]:
|
|
94
|
+
if info["relevance"] != "call":
|
|
95
|
+
scrollto_calls.append(f"tracerite_scrollto('{info['id']}')")
|
|
96
|
+
break
|
|
97
|
+
doc._script(javascript + "\n" + "\n".join(scrollto_calls))
|
|
98
|
+
return doc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _exception(doc, info, *, local_urls=False, chain_suffix=""):
|
|
102
|
+
"""Format single exception message and traceback"""
|
|
103
|
+
summary, message = info["summary"], info["message"]
|
|
104
|
+
doc.h3(E.span(f"{info['type']}{chain_suffix}:", class_="exctype")(f" {summary}"))
|
|
105
|
+
if summary != message:
|
|
106
|
+
if message.startswith(summary):
|
|
107
|
+
message = message[len(summary) :]
|
|
108
|
+
doc.pre(message, class_="excmessage")
|
|
109
|
+
# Traceback available?
|
|
110
|
+
frames = info["frames"]
|
|
111
|
+
if not frames:
|
|
112
|
+
return
|
|
113
|
+
# Format call chain, suppress middle of consecutive call runs if too long
|
|
114
|
+
limitedframes = _collapse_call_runs(frames, min_run_length=10)
|
|
115
|
+
# Collect symbols for floating indicators
|
|
116
|
+
frame_symbols = []
|
|
117
|
+
for frinfo in limitedframes:
|
|
118
|
+
if frinfo is not ... and frinfo["relevance"] != "call":
|
|
119
|
+
frame_symbols.append((frinfo["id"], symbols.get(frinfo["relevance"], "")))
|
|
120
|
+
with doc.div(class_="traceback-wrapper"):
|
|
121
|
+
# Floating overlay symbols (one per frame, positioned dynamically)
|
|
122
|
+
for fid, sym in frame_symbols:
|
|
123
|
+
with doc.span(class_="floating-symbol", data_frame=fid):
|
|
124
|
+
doc.span("◂", class_="arrow arrow-left")
|
|
125
|
+
doc.span(sym, class_="sym")
|
|
126
|
+
doc.span("▸", class_="arrow arrow-right")
|
|
127
|
+
with doc.div(class_="traceback-frames"):
|
|
128
|
+
# Render frames
|
|
129
|
+
for frinfo in limitedframes:
|
|
130
|
+
if frinfo is ...:
|
|
131
|
+
with doc.div(class_="traceback-details traceback-ellipsis"):
|
|
132
|
+
doc.p("...")
|
|
133
|
+
continue
|
|
134
|
+
attrs = {
|
|
135
|
+
"class_": "traceback-details",
|
|
136
|
+
"data_function": frinfo["function"],
|
|
137
|
+
"id": frinfo["id"],
|
|
138
|
+
}
|
|
139
|
+
with doc.div(**attrs):
|
|
140
|
+
_frame_label(doc, frinfo, local_urls=local_urls)
|
|
141
|
+
with doc.div(class_="frame-content"):
|
|
142
|
+
traceback_detail(doc, info, frinfo)
|
|
143
|
+
variable_inspector(doc, frinfo["variables"])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _frame_label(doc, frinfo, *, local_urls=False):
|
|
147
|
+
"""Render sticky label for a code frame with optional editor links."""
|
|
148
|
+
# Build title text: full path with line number
|
|
149
|
+
if frinfo["filename"]:
|
|
150
|
+
lineno = frinfo["range"].lfirst if frinfo["range"] else "?"
|
|
151
|
+
title = f"{frinfo['filename']}:{lineno}"
|
|
152
|
+
else:
|
|
153
|
+
title = frinfo["location"] or frinfo["function"] or ""
|
|
154
|
+
|
|
155
|
+
with doc.div(class_="frame-label", title=title):
|
|
156
|
+
doc.strong(frinfo["location"])
|
|
157
|
+
if frinfo["function"]:
|
|
158
|
+
doc(" ")
|
|
159
|
+
doc.span(frinfo["function"], class_="frame-function")
|
|
160
|
+
# Editor links if available
|
|
161
|
+
urls = frinfo.get("urls", {})
|
|
162
|
+
if local_urls and urls:
|
|
163
|
+
for name, href in urls.items():
|
|
164
|
+
doc.a(name, href=href, class_="frame-link")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def traceback_detail(doc, info, frinfo):
|
|
168
|
+
# Code printout
|
|
169
|
+
fragments = frinfo.get("fragments", [])
|
|
170
|
+
if not fragments:
|
|
171
|
+
doc.p("Source code not available")
|
|
172
|
+
if frinfo is info["frames"][-1]:
|
|
173
|
+
doc(" but ").strong(info["type"])(" was raised from here")
|
|
174
|
+
else:
|
|
175
|
+
with doc.pre, doc.code:
|
|
176
|
+
start = frinfo["linenostart"]
|
|
177
|
+
for line_info in fragments:
|
|
178
|
+
line_num = line_info["line"]
|
|
179
|
+
abs_line = start + line_num - 1
|
|
180
|
+
fragments = line_info["fragments"]
|
|
181
|
+
|
|
182
|
+
# Prepare tooltip attributes for tooltip span on final line
|
|
183
|
+
tooltip_attrs = {}
|
|
184
|
+
if frinfo["range"] and abs_line == frinfo["range"].lfinal:
|
|
185
|
+
relevance = frinfo["relevance"]
|
|
186
|
+
symbol = symbols.get(relevance, frinfo["relevance"])
|
|
187
|
+
try:
|
|
188
|
+
text = tooltips[relevance].format(**info, **frinfo)
|
|
189
|
+
# Replace newlines with spaces for HTML attribute
|
|
190
|
+
text = text.replace("\n", " ")
|
|
191
|
+
except Exception:
|
|
192
|
+
text = repr(relevance)
|
|
193
|
+
tooltip_attrs = {
|
|
194
|
+
"class": "tracerite-tooltip",
|
|
195
|
+
"data-symbol": symbol,
|
|
196
|
+
"data-tooltip": text,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Render content fragments inside the codeline span
|
|
200
|
+
with doc.span(class_="codeline", data_lineno=abs_line):
|
|
201
|
+
# Find the first non-trailing fragment to start the tooltip span
|
|
202
|
+
non_trailing_fragments = []
|
|
203
|
+
trailing_fragment = None
|
|
204
|
+
for fragment in fragments:
|
|
205
|
+
if "trailing" in fragment:
|
|
206
|
+
trailing_fragment = fragment
|
|
207
|
+
break
|
|
208
|
+
non_trailing_fragments.append(fragment)
|
|
209
|
+
|
|
210
|
+
# Render leading whitespace/indentation first (outside tooltip span)
|
|
211
|
+
if non_trailing_fragments:
|
|
212
|
+
first_fragment = non_trailing_fragments[0]
|
|
213
|
+
code = first_fragment["code"]
|
|
214
|
+
leading_whitespace = code[: len(code) - len(code.lstrip())]
|
|
215
|
+
if leading_whitespace:
|
|
216
|
+
doc(leading_whitespace)
|
|
217
|
+
# Create modified first fragment without leading whitespace
|
|
218
|
+
first_fragment_modified = {
|
|
219
|
+
**first_fragment,
|
|
220
|
+
"code": code.lstrip(),
|
|
221
|
+
}
|
|
222
|
+
non_trailing_fragments[0] = first_fragment_modified
|
|
223
|
+
|
|
224
|
+
# Render the tooltip span around the actual code content
|
|
225
|
+
if tooltip_attrs and non_trailing_fragments:
|
|
226
|
+
with doc.span(
|
|
227
|
+
class_="tracerite-tooltip",
|
|
228
|
+
data_tooltip=tooltip_attrs["data-tooltip"],
|
|
229
|
+
):
|
|
230
|
+
for fragment in non_trailing_fragments:
|
|
231
|
+
_render_fragment(doc, fragment)
|
|
232
|
+
# Add separate symbol and tooltip text elements
|
|
233
|
+
doc.span(
|
|
234
|
+
class_="tracerite-symbol",
|
|
235
|
+
data_symbol=tooltip_attrs["data-symbol"],
|
|
236
|
+
)
|
|
237
|
+
doc.span(
|
|
238
|
+
class_="tracerite-tooltip-text",
|
|
239
|
+
data_tooltip=tooltip_attrs["data-tooltip"],
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
for fragment in non_trailing_fragments:
|
|
243
|
+
_render_fragment(doc, fragment)
|
|
244
|
+
|
|
245
|
+
# Set fragment for trailing handling
|
|
246
|
+
fragment = trailing_fragment
|
|
247
|
+
# Render trailing fragment outside the span
|
|
248
|
+
if fragment:
|
|
249
|
+
_render_fragment(doc, fragment)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _render_fragment(doc, fragment):
|
|
253
|
+
"""Render a single fragment with appropriate styling."""
|
|
254
|
+
code = fragment["code"]
|
|
255
|
+
|
|
256
|
+
mark = fragment.get("mark")
|
|
257
|
+
em = fragment.get("em")
|
|
258
|
+
|
|
259
|
+
# Render opening tags for "mark" and "em" if applicable
|
|
260
|
+
if mark in ["solo", "beg"]:
|
|
261
|
+
doc(HTML("<mark>"))
|
|
262
|
+
if em in ["solo", "beg"]:
|
|
263
|
+
doc(HTML("<em>"))
|
|
264
|
+
|
|
265
|
+
# Render the code
|
|
266
|
+
doc(code)
|
|
267
|
+
|
|
268
|
+
# Render closing tags for "mark" and "em" if applicable
|
|
269
|
+
if em in ["fin", "solo"]:
|
|
270
|
+
doc(HTML("</em>"))
|
|
271
|
+
if mark in ["fin", "solo"]:
|
|
272
|
+
doc(HTML("</mark>"))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def variable_inspector(doc, variables):
|
|
276
|
+
if not variables:
|
|
277
|
+
return
|
|
278
|
+
with doc.table(class_="inspector key-value"):
|
|
279
|
+
for var_info in variables:
|
|
280
|
+
# Handle both old tuple format and new VarInfo namedtuple
|
|
281
|
+
if hasattr(var_info, "name"):
|
|
282
|
+
n, t, v, fmt = (
|
|
283
|
+
var_info.name,
|
|
284
|
+
var_info.typename,
|
|
285
|
+
var_info.value,
|
|
286
|
+
var_info.format_hint,
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
# Backwards compatibility with old tuple format
|
|
290
|
+
n, t, v = var_info
|
|
291
|
+
fmt = "inline"
|
|
292
|
+
|
|
293
|
+
doc.tr.td.span(n, class_="var")
|
|
294
|
+
if t:
|
|
295
|
+
doc(": ").span(f"{t}\u00a0=\u00a0", class_="type")
|
|
296
|
+
else:
|
|
297
|
+
doc("\u00a0").span("=\u00a0", class_="type") # No type printed
|
|
298
|
+
doc.td(class_=f"val val-{fmt}")
|
|
299
|
+
if isinstance(v, str):
|
|
300
|
+
if fmt == "block":
|
|
301
|
+
# For block format, use <pre> tag for proper formatting
|
|
302
|
+
doc.pre(v)
|
|
303
|
+
else:
|
|
304
|
+
doc(v)
|
|
305
|
+
elif isinstance(v, dict) and v.get("type") == "keyvalue":
|
|
306
|
+
_format_keyvalue(doc, v["rows"])
|
|
307
|
+
elif isinstance(v, dict) and v.get("type") == "array":
|
|
308
|
+
with doc.div(class_="array-with-scale"):
|
|
309
|
+
_format_matrix(doc, v["rows"])
|
|
310
|
+
if v.get("suffix"):
|
|
311
|
+
doc.span(v["suffix"], class_="scale-suffix")
|
|
312
|
+
else:
|
|
313
|
+
_format_matrix(doc, v)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _format_keyvalue(doc, rows):
|
|
317
|
+
"""Format key-value pairs (dicts, dataclasses) as a definition list."""
|
|
318
|
+
with doc.dl(class_="keyvalue-dl"):
|
|
319
|
+
for key, val in rows:
|
|
320
|
+
doc.dt(key)
|
|
321
|
+
doc.dd(val)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _format_matrix(doc, v):
|
|
325
|
+
skipcol = skiprow = False
|
|
326
|
+
with doc.table:
|
|
327
|
+
for row in v:
|
|
328
|
+
if row[0] is None:
|
|
329
|
+
skiprow = True
|
|
330
|
+
continue
|
|
331
|
+
doc.tr()
|
|
332
|
+
if skiprow:
|
|
333
|
+
skiprow = False
|
|
334
|
+
doc(class_="skippedabove")
|
|
335
|
+
for e in row:
|
|
336
|
+
if e is None:
|
|
337
|
+
skipcol = True
|
|
338
|
+
continue
|
|
339
|
+
if skipcol:
|
|
340
|
+
skipcol = False
|
|
341
|
+
doc.td(e, class_="skippedleft")
|
|
342
|
+
else:
|
|
343
|
+
doc.td(e)
|