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,3 @@
1
+ from .serializer import save_traceback, load_traceback
2
+
3
+ __all__ = ["save_traceback", "load_traceback"]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any