offline-debug 0.1.1__tar.gz → 0.2.1__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,11 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: offline-debug
3
- Version: 0.1.1
3
+ Version: 0.2.1
4
4
  Summary: Debug exceptions offline by saving them to a dump and raising them at a later point.
5
5
  Author: Itai Elidan
6
6
  Author-email: Itai Elidan <itaielidan@gmail.com>
7
7
  Requires-Dist: typing-extensions>=4.15.0
8
- Requires-Python: >=3.12
8
+ Requires-Python: >=3.12, <3.14
9
9
  Project-URL: Homepage, https://github.com/INTODAN/offline-debug
10
10
  Project-URL: Repository, https://github.com/INTODAN/offline-debug
11
11
  Description-Content-Type: text/markdown
@@ -16,43 +16,60 @@ Description-Content-Type: text/markdown
16
16
  [![Tests](https://github.com/INTODAN/offline-debug/actions/workflows/ci.yml/badge.svg)](https://github.com/INTODAN/offline-debug/actions)
17
17
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/INTODAN/offline-debug)
18
18
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
19
- [![Ty checked](https://img.shields.io/badge/ty-checked-blue.svg)](https://github.com/intodan/ty)
19
+ [![Ty checked](https://img.shields.io/badge/ty-checked-blue.svg)](https://github.com/astral-sh/ty)
20
20
 
21
21
  ## Overview
22
- A Python package for high-fidelity serialization and deserialization of exceptions and their complete tracebacks. Unlike other solutions, `offline-debug` reconstructs **actual** `types.FrameType` objects using the Python C API, ensuring that re-raised exceptions look and feel genuine to debuggers and introspection tools.
22
+
23
+ A Python package for high-fidelity serialization and deserialization of exceptions and their complete tracebacks. Unlike other
24
+ solutions, `offline-debug` reconstructs **actual** `types.FrameType` objects using the Python C API, ensuring that re-raised
25
+ exceptions look and feel genuine to debuggers and introspection tools.
23
26
 
24
27
  ## Core Functions
25
- - `save_traceback(exc: Exception, file_path: str)`:
26
- Serializes an exception, its traceback, and all picklable local/global variables to a binary file.
27
- - `load_traceback(file_path: str) -> typing.Never`:
28
- Loads the serialized state, reconstructs the exception and its full traceback chain (including `__cause__` and `__context__`), and raises it.
29
28
 
30
- ## Technical Implementation
31
- - **True Frame Reconstruction**: Uses `ctypes` to call `PyFrame_New` from the Python C API. This creates real `frame` objects which are required for a valid `types.TracebackType`.
32
- - **Python 3.13 Compatibility**: Leverages PEP 667 features where `f_locals` is a write-through proxy, allowing for accurate local variable restoration.
33
- - **Robust Serialization**:
34
- - `pickle` is used for exceptions and variables.
35
- - `marshal` is used for code objects.
36
- - Non-picklable items are gracefully handled by storing their `repr`.
37
-
38
- ## Development & Tooling
39
- - **Package Manager**: `uv`
40
- - **Minimum Python**: 3.12
41
- - **Testing**: `pytest`
42
- - **Commands**:
43
- - Add dependencies: `uv add <package>`
44
- - Run tests: `uv run pytest`
29
+ - `save_traceback(exc: BaseException, file: Path | BytesIO)`:
30
+ Serializes an exception, its traceback, and all picklable local/global variables to a binary file or buffer.
31
+ - `load_traceback(file: Path | BytesIO) -> Never`:
32
+ Loads the serialized state, reconstructs the exception and its full traceback chain (including `__cause__` and `__context__`),
33
+ and raises it.
45
34
 
46
35
  ## Usage Example
36
+
37
+ to get started, install with:
38
+ `pip install offline-debug` or `uv add offline-debug`
39
+
47
40
  ```python
41
+ from pathlib import Path
48
42
  from offline_debug import save_traceback, load_traceback
49
43
 
50
44
  try:
51
45
  # Code that might fail
52
46
  some_complex_operation()
53
47
  except Exception as e:
54
- save_traceback(e, "crash_report.dump")
48
+ save_traceback(e, Path("crash_report.dump"))
55
49
 
56
50
  # To debug or re-examine later:
57
- load_traceback("crash_report.dump")
51
+ load_traceback(Path("crash_report.dump"))
58
52
  ```
53
+
54
+ ## Technical Implementation
55
+
56
+ - **True Frame Reconstruction**: Uses `ctypes` to call `PyFrame_New` from the Python C API. This creates real `frame` objects
57
+ which are required for a valid `types.TracebackType`.
58
+ - **Python 3.13 Compatibility**: Leverages PEP 667 features where `f_locals` is a write-through proxy, allowing for accurate local
59
+ variable restoration.
60
+ - **Support python 3.12 as well**
61
+ - **Robust Serialization**:
62
+ - `pickle` is used for exceptions and variables.
63
+ - `marshal` is used for code objects.
64
+ - Non-picklable items are gracefully handled by storing their `repr`.
65
+
66
+ ## Development & Tooling
67
+
68
+ - **Package Manager**: `uv`
69
+ - **Minimum Python**: 3.12
70
+ - **Testing**: `pytest`
71
+ - **Commands**:
72
+ - Add dependencies: `uv add <package>`
73
+ - Run tests: `uv run pytest`
74
+
75
+
@@ -4,43 +4,60 @@
4
4
  [![Tests](https://github.com/INTODAN/offline-debug/actions/workflows/ci.yml/badge.svg)](https://github.com/INTODAN/offline-debug/actions)
5
5
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/INTODAN/offline-debug)
6
6
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
7
- [![Ty checked](https://img.shields.io/badge/ty-checked-blue.svg)](https://github.com/intodan/ty)
7
+ [![Ty checked](https://img.shields.io/badge/ty-checked-blue.svg)](https://github.com/astral-sh/ty)
8
8
 
9
9
  ## Overview
10
- A Python package for high-fidelity serialization and deserialization of exceptions and their complete tracebacks. Unlike other solutions, `offline-debug` reconstructs **actual** `types.FrameType` objects using the Python C API, ensuring that re-raised exceptions look and feel genuine to debuggers and introspection tools.
10
+
11
+ A Python package for high-fidelity serialization and deserialization of exceptions and their complete tracebacks. Unlike other
12
+ solutions, `offline-debug` reconstructs **actual** `types.FrameType` objects using the Python C API, ensuring that re-raised
13
+ exceptions look and feel genuine to debuggers and introspection tools.
11
14
 
12
15
  ## Core Functions
13
- - `save_traceback(exc: Exception, file_path: str)`:
14
- Serializes an exception, its traceback, and all picklable local/global variables to a binary file.
15
- - `load_traceback(file_path: str) -> typing.Never`:
16
- Loads the serialized state, reconstructs the exception and its full traceback chain (including `__cause__` and `__context__`), and raises it.
17
16
 
18
- ## Technical Implementation
19
- - **True Frame Reconstruction**: Uses `ctypes` to call `PyFrame_New` from the Python C API. This creates real `frame` objects which are required for a valid `types.TracebackType`.
20
- - **Python 3.13 Compatibility**: Leverages PEP 667 features where `f_locals` is a write-through proxy, allowing for accurate local variable restoration.
21
- - **Robust Serialization**:
22
- - `pickle` is used for exceptions and variables.
23
- - `marshal` is used for code objects.
24
- - Non-picklable items are gracefully handled by storing their `repr`.
25
-
26
- ## Development & Tooling
27
- - **Package Manager**: `uv`
28
- - **Minimum Python**: 3.12
29
- - **Testing**: `pytest`
30
- - **Commands**:
31
- - Add dependencies: `uv add <package>`
32
- - Run tests: `uv run pytest`
17
+ - `save_traceback(exc: BaseException, file: Path | BytesIO)`:
18
+ Serializes an exception, its traceback, and all picklable local/global variables to a binary file or buffer.
19
+ - `load_traceback(file: Path | BytesIO) -> Never`:
20
+ Loads the serialized state, reconstructs the exception and its full traceback chain (including `__cause__` and `__context__`),
21
+ and raises it.
33
22
 
34
23
  ## Usage Example
24
+
25
+ to get started, install with:
26
+ `pip install offline-debug` or `uv add offline-debug`
27
+
35
28
  ```python
29
+ from pathlib import Path
36
30
  from offline_debug import save_traceback, load_traceback
37
31
 
38
32
  try:
39
33
  # Code that might fail
40
34
  some_complex_operation()
41
35
  except Exception as e:
42
- save_traceback(e, "crash_report.dump")
36
+ save_traceback(e, Path("crash_report.dump"))
43
37
 
44
38
  # To debug or re-examine later:
45
- load_traceback("crash_report.dump")
39
+ load_traceback(Path("crash_report.dump"))
46
40
  ```
41
+
42
+ ## Technical Implementation
43
+
44
+ - **True Frame Reconstruction**: Uses `ctypes` to call `PyFrame_New` from the Python C API. This creates real `frame` objects
45
+ which are required for a valid `types.TracebackType`.
46
+ - **Python 3.13 Compatibility**: Leverages PEP 667 features where `f_locals` is a write-through proxy, allowing for accurate local
47
+ variable restoration.
48
+ - **Support python 3.12 as well**
49
+ - **Robust Serialization**:
50
+ - `pickle` is used for exceptions and variables.
51
+ - `marshal` is used for code objects.
52
+ - Non-picklable items are gracefully handled by storing their `repr`.
53
+
54
+ ## Development & Tooling
55
+
56
+ - **Package Manager**: `uv`
57
+ - **Minimum Python**: 3.12
58
+ - **Testing**: `pytest`
59
+ - **Commands**:
60
+ - Add dependencies: `uv add <package>`
61
+ - Run tests: `uv run pytest`
62
+
63
+
@@ -0,0 +1,7 @@
1
+ """Tool for serializing and reconstructing Python exceptions with full stack traces."""
2
+
3
+ from ._inner.load_traceback import load_traceback, parse_traceback
4
+ from ._inner.models import ExceptionData, FrameData
5
+ from ._inner.save_traceback import save_traceback
6
+
7
+ __all__ = ["ExceptionData", "FrameData", "load_traceback", "parse_traceback", "save_traceback"]
@@ -7,19 +7,18 @@ import types
7
7
  from io import BytesIO
8
8
  from pathlib import Path
9
9
  from types import CodeType
10
- from typing import Never
11
10
 
12
11
  from offline_debug._inner.c_api import (
13
12
  create_frame,
14
13
  link_frame,
15
14
  )
16
15
  from offline_debug._inner.models import (
17
- _ExceptionData,
18
- _FrameData,
16
+ ExceptionData,
17
+ FrameData,
19
18
  )
20
19
 
21
20
 
22
- def _reconstruct_exc_data(data: _ExceptionData) -> BaseException:
21
+ def _reconstruct_exc_data(data: ExceptionData) -> BaseException:
23
22
  """
24
23
  Recursively reconstruct an exception from its serialized data.
25
24
 
@@ -37,26 +36,25 @@ def _reconstruct_exc_data(data: _ExceptionData) -> BaseException:
37
36
  msg = f"Expected BaseException, but got {type(exc).__name__}"
38
37
  raise TypeError(msg)
39
38
 
40
- reconstructed_frames: list[tuple[types.FrameType, _FrameData]] = []
39
+ reconstructed_frames: list[tuple[types.FrameType, FrameData]] = []
41
40
  for f_data in data.tb_frames:
42
41
  code: CodeType = marshal.loads(f_data.code) # noqa: S302
43
42
 
44
- # In Python 3.11 and 3.12, accessing f_locals on a frame created via
43
+ # In Python 3.11+, accessing f_locals on a frame created via
45
44
  # PyFrame_New for optimized code (functions) causes a segmentation fault
46
45
  # because the internal 'fast' locals array is not initialized.
47
46
  # As a workaround, we create a 'non-optimized' version of the code object
48
47
  # by compiling a dummy string. This ensures the bytecode is safe
49
48
  # (no LOAD_FAST) while preserving metadata like name and filename.
50
- if sys.version_info < (3, 13):
51
- # A simple module-level code object never has fast locals.
52
- # Since the source is empty, no optimized locals will be created.
53
- # Instead, python will go to the unoptimized dictionary we set under frame_locals later.
54
- unoptimized_code = compile("", code.co_filename, "exec")
55
- code = unoptimized_code.replace(
56
- co_name=code.co_name,
57
- co_firstlineno=code.co_firstlineno,
58
- co_qualname=code.co_qualname,
59
- )
49
+ # A simple module-level code object never has fast locals.
50
+ # Since the source is empty, no optimized locals will be created.
51
+ # Instead, python will go to the unoptimized dictionary we set under frame_locals later.
52
+ unoptimized_code = compile("", code.co_filename, "exec")
53
+ code = unoptimized_code.replace(
54
+ co_name=code.co_name,
55
+ co_firstlineno=code.co_firstlineno,
56
+ co_qualname=code.co_qualname,
57
+ )
60
58
 
61
59
  # PyFrame_New returns a new reference to a PyFrameObject.
62
60
  if f_data.module_name:
@@ -92,17 +90,22 @@ def _reconstruct_exc_data(data: _ExceptionData) -> BaseException:
92
90
  return exc
93
91
 
94
92
 
95
- def load_traceback(file: Path | BytesIO) -> Never:
96
- """Load an exception and its traceback from a file and raise it."""
93
+ def parse_traceback(file: Path | BytesIO) -> ExceptionData:
97
94
  if isinstance(file, Path):
98
95
  with file.open("rb") as f:
99
96
  data = pickle.load(f) # noqa: S301
100
97
  else:
101
98
  data = pickle.load(file) # noqa: S301
102
99
 
103
- if not isinstance(data, _ExceptionData):
100
+ if not isinstance(data, ExceptionData):
104
101
  msg = f"Expected _ExceptionData, but got {type(data).__name__}"
105
102
  raise TypeError(msg)
103
+ return data
104
+
105
+
106
+ def load_traceback(file: Path | BytesIO, should_raise: bool = True) -> BaseException: # noqa: FBT001, FBT002
107
+ """Load an exception and its traceback from a file and raise it."""
108
+ data = parse_traceback(file)
106
109
 
107
110
  exc = _reconstruct_exc_data(data)
108
111
 
@@ -126,4 +129,6 @@ def load_traceback(file: Path | BytesIO) -> Never:
126
129
  )
127
130
 
128
131
  exc = exc.with_traceback(tb_chain)
129
- raise exc
132
+ if should_raise:
133
+ raise exc
134
+ return exc
@@ -7,7 +7,7 @@ from typing import Any
7
7
 
8
8
 
9
9
  @dataclass
10
- class _FrameData:
10
+ class FrameData:
11
11
  """Serialized data for a single stack frame."""
12
12
 
13
13
  code: bytes
@@ -20,10 +20,10 @@ class _FrameData:
20
20
 
21
21
 
22
22
  @dataclass
23
- class _ExceptionData:
23
+ class ExceptionData:
24
24
  """Serialized data for an exception and its traceback."""
25
25
 
26
26
  exc_pickle: bytes
27
- tb_frames: list[_FrameData]
28
- cause: _ExceptionData | None = None
29
- context: _ExceptionData | None = None
27
+ tb_frames: list[FrameData]
28
+ cause: ExceptionData | None = None
29
+ context: ExceptionData | None = None
@@ -6,7 +6,7 @@ import types
6
6
  from io import BytesIO
7
7
  from pathlib import Path
8
8
 
9
- from offline_debug._inner.models import _ExceptionData, _FrameData
9
+ from offline_debug._inner.models import ExceptionData, FrameData
10
10
 
11
11
  # Internal attributes that are either unpicklable or redundant in a new process.
12
12
  # We exclude these specifically because they are automatically recreated
@@ -41,9 +41,9 @@ def _filter_dict(d: dict) -> dict:
41
41
  return result
42
42
 
43
43
 
44
- def _serialize_exc_data(exc: BaseException) -> _ExceptionData:
44
+ def _serialize_exc_data(exc: BaseException) -> ExceptionData:
45
45
  """Recursively serialize exception data into dataclasses."""
46
- tb_frames: list[_FrameData] = []
46
+ tb_frames: list[FrameData] = []
47
47
  curr_tb = exc.__traceback__
48
48
  while curr_tb:
49
49
  f = curr_tb.tb_frame
@@ -58,7 +58,7 @@ def _serialize_exc_data(exc: BaseException) -> _ExceptionData:
58
58
  mod_name = spec.name
59
59
 
60
60
  tb_frames.append(
61
- _FrameData(
61
+ FrameData(
62
62
  code=marshal.dumps(f.f_code),
63
63
  globals=_filter_dict(f.f_globals),
64
64
  locals=_filter_dict(f.f_locals),
@@ -77,7 +77,7 @@ def _serialize_exc_data(exc: BaseException) -> _ExceptionData:
77
77
  RuntimeError(f"Unpicklable exception {type(exc).__name__}: {exc!s}")
78
78
  )
79
79
 
80
- return _ExceptionData(
80
+ return ExceptionData(
81
81
  exc_pickle=exc_pickle,
82
82
  tb_frames=tb_frames,
83
83
  cause=_serialize_exc_data(exc.__cause__) if exc.__cause__ else None,
@@ -85,9 +85,12 @@ def _serialize_exc_data(exc: BaseException) -> _ExceptionData:
85
85
  )
86
86
 
87
87
 
88
- def save_traceback(exc: BaseException, file: Path | BytesIO) -> None:
88
+ def save_traceback(exc: BaseException, file: Path | BytesIO | None) -> ExceptionData:
89
89
  """Serialize an exception and its traceback to a file."""
90
90
  data = _serialize_exc_data(exc)
91
+ if file is None:
92
+ return data
93
+
91
94
  if isinstance(file, Path):
92
95
  with file.open("wb") as f:
93
96
  pickle.dump(data, f)
@@ -96,3 +99,4 @@ def save_traceback(exc: BaseException, file: Path | BytesIO) -> None:
96
99
  else:
97
100
  msg = f"Unexpected type for file {type(file).__name__}"
98
101
  raise TypeError(msg)
102
+ return data
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "offline-debug"
3
- version = "0.1.1"
3
+ version = "0.2.1"
4
4
  description = "Debug exceptions offline by saving them to a dump and raising them at a later point."
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Itai Elidan", email = "itaielidan@gmail.com" }
8
8
  ]
9
- requires-python = ">=3.12"
9
+ requires-python = ">=3.12, <3.14"
10
10
  dependencies = [
11
11
  "typing-extensions>=4.15.0",
12
12
  ]
@@ -1,6 +0,0 @@
1
- """Tool for serializing and reconstructing Python exceptions with full stack traces."""
2
-
3
- from ._inner.load_traceback import load_traceback
4
- from ._inner.save_traceback import save_traceback
5
-
6
- __all__ = ["load_traceback", "save_traceback"]