offline-debug 0.1.0b1__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.
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import marshal
|
|
3
|
+
import types
|
|
4
|
+
import typing
|
|
5
|
+
import ctypes
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
# Define C API for frame creation
|
|
9
|
+
_py_frame_new = ctypes.pythonapi.PyFrame_New
|
|
10
|
+
_py_frame_new.argtypes = (
|
|
11
|
+
ctypes.c_void_p, # PyThreadState *tstate
|
|
12
|
+
ctypes.py_object, # PyCodeObject *code
|
|
13
|
+
ctypes.py_object, # PyObject *globals
|
|
14
|
+
ctypes.py_object, # PyObject *locals
|
|
15
|
+
)
|
|
16
|
+
_py_frame_new.restype = ctypes.py_object
|
|
17
|
+
|
|
18
|
+
_py_thread_state_get = ctypes.pythonapi.PyThreadState_Get
|
|
19
|
+
_py_thread_state_get.restype = ctypes.c_void_p
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Define ctypes structure to access f_back in PyFrameObject
|
|
23
|
+
class _PyObject(ctypes.Structure):
|
|
24
|
+
_fields_ = [("ob_refcnt", ctypes.c_ssize_t), ("ob_type", ctypes.c_void_p)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _PyFrameObject(ctypes.Structure):
|
|
28
|
+
_fields_ = [
|
|
29
|
+
("ob_base", _PyObject),
|
|
30
|
+
("f_back", ctypes.c_void_p), # Pointer to previous frame
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_stack_depth(frame):
|
|
35
|
+
depth = 0
|
|
36
|
+
while frame:
|
|
37
|
+
depth += 1
|
|
38
|
+
frame = frame.f_back
|
|
39
|
+
return depth
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _filter_dict(d: dict) -> dict:
|
|
43
|
+
"""Filter dictionary to include only picklable items."""
|
|
44
|
+
result = {}
|
|
45
|
+
for k, v in d.items():
|
|
46
|
+
if k in ("__builtins__", "__doc__", "__loader__", "__package__", "__spec__"):
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
pickle.dumps(v)
|
|
50
|
+
result[k] = v
|
|
51
|
+
except Exception:
|
|
52
|
+
result[k] = f"<unpicklable {type(v).__name__}: {repr(v)}>"
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _serialize_exc_data(exc: BaseException) -> dict:
|
|
57
|
+
tb_frames = []
|
|
58
|
+
curr_tb = exc.__traceback__
|
|
59
|
+
while curr_tb:
|
|
60
|
+
f = curr_tb.tb_frame
|
|
61
|
+
tb_frames.append(
|
|
62
|
+
{
|
|
63
|
+
"code": marshal.dumps(f.f_code),
|
|
64
|
+
"globals": _filter_dict(f.f_globals),
|
|
65
|
+
"locals": _filter_dict(f.f_locals),
|
|
66
|
+
"lasti": curr_tb.tb_lasti,
|
|
67
|
+
"lineno": curr_tb.tb_lineno,
|
|
68
|
+
"stack_depth": _get_stack_depth(f),
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
curr_tb = curr_tb.tb_next
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
exc_pickle = pickle.dumps(exc)
|
|
75
|
+
except Exception:
|
|
76
|
+
exc_pickle = pickle.dumps(
|
|
77
|
+
RuntimeError(f"Unpicklable exception {type(exc).__name__}: {str(exc)}")
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"exc_pickle": exc_pickle,
|
|
82
|
+
"tb_frames": tb_frames,
|
|
83
|
+
"cause": _serialize_exc_data(exc.__cause__) if exc.__cause__ else None,
|
|
84
|
+
"context": _serialize_exc_data(exc.__context__) if exc.__context__ else None,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def save_traceback(exc: Exception, file_path: str):
|
|
89
|
+
"""Serialize an exception and its traceback to a file."""
|
|
90
|
+
data = _serialize_exc_data(exc)
|
|
91
|
+
with open(file_path, "wb") as f:
|
|
92
|
+
pickle.dump(data, f)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _reconstruct_exc_data(data: dict) -> Exception:
|
|
96
|
+
exc = pickle.loads(data["exc_pickle"])
|
|
97
|
+
|
|
98
|
+
tstate = _py_thread_state_get()
|
|
99
|
+
|
|
100
|
+
reconstructed_frames = []
|
|
101
|
+
prev_frame = None
|
|
102
|
+
for f_data in data["tb_frames"]:
|
|
103
|
+
code = marshal.loads(f_data["code"])
|
|
104
|
+
|
|
105
|
+
frame = _py_frame_new(tstate, code, f_data["globals"], {})
|
|
106
|
+
|
|
107
|
+
if f_data["locals"]:
|
|
108
|
+
frame.f_locals.update(f_data["locals"])
|
|
109
|
+
|
|
110
|
+
if prev_frame:
|
|
111
|
+
frame_ptr = _PyFrameObject.from_address(id(frame))
|
|
112
|
+
frame_ptr.f_back = id(prev_frame)
|
|
113
|
+
|
|
114
|
+
reconstructed_frames.append((frame, f_data))
|
|
115
|
+
prev_frame = frame
|
|
116
|
+
|
|
117
|
+
tb_next = None
|
|
118
|
+
for frame, f_data in reversed(reconstructed_frames):
|
|
119
|
+
tb = types.TracebackType(
|
|
120
|
+
tb_next=tb_next,
|
|
121
|
+
tb_frame=frame,
|
|
122
|
+
tb_lasti=f_data["lasti"],
|
|
123
|
+
tb_lineno=f_data["lineno"],
|
|
124
|
+
)
|
|
125
|
+
tb_next = tb
|
|
126
|
+
|
|
127
|
+
exc = exc.with_traceback(tb_next)
|
|
128
|
+
|
|
129
|
+
if data["cause"]:
|
|
130
|
+
exc.__cause__ = _reconstruct_exc_data(data["cause"])
|
|
131
|
+
if data["context"]:
|
|
132
|
+
exc.__context__ = _reconstruct_exc_data(data["context"])
|
|
133
|
+
|
|
134
|
+
return exc
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def load_traceback(file_path: str) -> typing.Never:
|
|
138
|
+
"""Load an exception and its traceback from a file and raise it."""
|
|
139
|
+
with open(file_path, "rb") as f:
|
|
140
|
+
data = pickle.load(f)
|
|
141
|
+
|
|
142
|
+
exc = _reconstruct_exc_data(data)
|
|
143
|
+
|
|
144
|
+
current_frames = []
|
|
145
|
+
curr = sys._getframe(1)
|
|
146
|
+
while curr:
|
|
147
|
+
current_frames.append(curr)
|
|
148
|
+
curr = curr.f_back
|
|
149
|
+
|
|
150
|
+
if exc.__traceback__ and current_frames:
|
|
151
|
+
reconstructed_outer = exc.__traceback__.tb_frame
|
|
152
|
+
caller_frame = current_frames[0]
|
|
153
|
+
|
|
154
|
+
frame_ptr = _PyFrameObject.from_address(id(reconstructed_outer))
|
|
155
|
+
frame_ptr.f_back = id(caller_frame)
|
|
156
|
+
|
|
157
|
+
tb_chain = exc.__traceback__
|
|
158
|
+
for frame in current_frames:
|
|
159
|
+
tb_chain = types.TracebackType(
|
|
160
|
+
tb_next=tb_chain,
|
|
161
|
+
tb_frame=frame,
|
|
162
|
+
tb_lasti=frame.f_lasti,
|
|
163
|
+
tb_lineno=frame.f_lineno,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
exc = exc.with_traceback(tb_chain)
|
|
167
|
+
raise exc
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: offline-debug
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Debug exceptions offline by saving them to a dump and raising them at a later point.
|
|
5
|
+
Author: Itai Elidan
|
|
6
|
+
Author-email: Itai Elidan <itaielidan@gmail.com>
|
|
7
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
8
|
+
Requires-Python: >=3.13
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
offline_debug/__init__.py,sha256=i87K1VHFearUSjk_OxfHW8zaQJYQt6hFF0T7gQOWogQ,103
|
|
2
|
+
offline_debug/serializer.py,sha256=zLaw3UBtPIJ_MI9E3T6JlMnbqwwSf1S1U7FQjRKyH9c,4692
|
|
3
|
+
offline_debug-0.1.0b1.dist-info/WHEEL,sha256=bEhYrD-rjlF0iRRHiAnfJ0mEjMsRwm29hhDD7yRgWCY,80
|
|
4
|
+
offline_debug-0.1.0b1.dist-info/METADATA,sha256=vj6wIXqV6U8GmkFfr2__INmQ_KNKZiphvmEV65J8bpE,287
|
|
5
|
+
offline_debug-0.1.0b1.dist-info/RECORD,,
|