with-err 1.0.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.
- with_err/__init__.py +7 -0
- with_err/utils.py +11 -0
- with_err/with_err.py +65 -0
- with_err-1.0.0.dist-info/METADATA +117 -0
- with_err-1.0.0.dist-info/RECORD +7 -0
- with_err-1.0.0.dist-info/WHEEL +4 -0
- with_err-1.0.0.dist-info/entry_points.txt +2 -0
with_err/__init__.py
ADDED
with_err/utils.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# https://share.gemini.google/BDvazjX2RWsE
|
|
2
|
+
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_err_strs(err: Exception | None) -> list[str]:
|
|
7
|
+
"""Accepts an exception instance and returns the full traceback as list[str]."""
|
|
8
|
+
if err is None:
|
|
9
|
+
return []
|
|
10
|
+
|
|
11
|
+
return traceback.format_exception(err)
|
with_err/with_err.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# https://share.gemini.google/BDvazjX2RWsE
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# @type_check_only
|
|
11
|
+
class RetWithErr[**P, R](Protocol):
|
|
12
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> tuple[R | None, Exception | None]:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def with_err[**P, R](*exceptions: type[Exception]) -> Callable[[Callable[P, R]], RetWithErr[P, R]]:
|
|
17
|
+
"""
|
|
18
|
+
Wraps a function to return (result, Exception) instead of raising.
|
|
19
|
+
|
|
20
|
+
XXX Reason repeating exceptions and Exception:
|
|
21
|
+
We don't want to create another layer of function tracestack.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def decorator(func: Callable[P, R]) -> Callable[P, tuple[R | None, Exception | None]]:
|
|
25
|
+
@wraps(func)
|
|
26
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> tuple[R | None, Exception | None]:
|
|
27
|
+
try:
|
|
28
|
+
result = func(*args, **kwargs)
|
|
29
|
+
return result, None
|
|
30
|
+
except exceptions as e:
|
|
31
|
+
# 1. Fetch exception's original internal traceback.
|
|
32
|
+
tb = sys.exc_info()[2]
|
|
33
|
+
|
|
34
|
+
# 2. Capture the caller frame executing func.
|
|
35
|
+
caller_frame = sys._getframe(1)
|
|
36
|
+
|
|
37
|
+
# 3. Create a parent traceback frame and link it above 'tb'.
|
|
38
|
+
combined_tb = types.TracebackType(
|
|
39
|
+
tb_next=tb,
|
|
40
|
+
tb_frame=caller_frame,
|
|
41
|
+
tb_lasti=caller_frame.f_lasti,
|
|
42
|
+
tb_lineno=caller_frame.f_lineno
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# 4. Attach the combined traceback back to the error instance
|
|
46
|
+
return None, e.with_traceback(combined_tb)
|
|
47
|
+
except Exception as e: # ruff: ignore[blind-except]
|
|
48
|
+
# 1. Fetch exception's original internal traceback.
|
|
49
|
+
tb = sys.exc_info()[2]
|
|
50
|
+
|
|
51
|
+
# 2. Capture the caller frame executing func.
|
|
52
|
+
caller_frame = sys._getframe(1)
|
|
53
|
+
|
|
54
|
+
# 3. Create a parent traceback frame and link it above 'tb'.
|
|
55
|
+
combined_tb = types.TracebackType(
|
|
56
|
+
tb_next=tb,
|
|
57
|
+
tb_frame=caller_frame,
|
|
58
|
+
tb_lasti=caller_frame.f_lasti,
|
|
59
|
+
tb_lineno=caller_frame.f_lineno
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 4. Attach the combined traceback back to the error instance
|
|
63
|
+
return None, e.with_traceback(combined_tb)
|
|
64
|
+
return wrapper
|
|
65
|
+
return decorator
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: with-err
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: converting `try-except` pattern to Go-like `result, err` pattern.
|
|
5
|
+
Keywords:
|
|
6
|
+
Author: Chuan-Heng Hsiao
|
|
7
|
+
Author-email: Chuan-Heng Hsiao <hsiao.chuanheng@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Project-URL: Repository, https://github.com/chhsiao1981/with-err
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# with-err
|
|
16
|
+
|
|
17
|
+
`with-err` is a python library that converts `try-except` pattern into Go-like `result, err` pattern. I feel `result, err` pattern easier to maintain in large projects.
|
|
18
|
+
|
|
19
|
+
## Getting Started
|
|
20
|
+
|
|
21
|
+
### Install
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pip install with-err
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
(with [`uv`](https://docs.astral.sh/uv/))
|
|
28
|
+
```sh
|
|
29
|
+
uv pip install with-err
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Use as Function
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import json
|
|
36
|
+
from with_err import with_err
|
|
37
|
+
|
|
38
|
+
json_loads_e = with_err()(json.loads)
|
|
39
|
+
|
|
40
|
+
data, err = json_loads_e('{"a": 1}')
|
|
41
|
+
assert err is None
|
|
42
|
+
assert data = {"a": 1}
|
|
43
|
+
|
|
44
|
+
data, err = json_loads_e('{"a": }')
|
|
45
|
+
assert err is not None
|
|
46
|
+
assert data is None
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Use as Decorator
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import json
|
|
53
|
+
from with_err import with_err
|
|
54
|
+
|
|
55
|
+
@with_err()
|
|
56
|
+
def json_loads_e(a: str | bytes | bytearray):
|
|
57
|
+
return json.loads(a)
|
|
58
|
+
|
|
59
|
+
data, err = json_loads_e('{"a": 1}')
|
|
60
|
+
assert err is None
|
|
61
|
+
assert data = {"a": 1}
|
|
62
|
+
|
|
63
|
+
data, err = json_loads_e('{"a": }')
|
|
64
|
+
assert err is not None
|
|
65
|
+
assert data is None
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `with_err` with Specified Exceptions
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import json
|
|
72
|
+
from with_err import with_err
|
|
73
|
+
|
|
74
|
+
@with_err(json.decoder.JSONDecodeError)
|
|
75
|
+
def json_loads_e(a: str | bytes | bytearray):
|
|
76
|
+
return json.loads(a)
|
|
77
|
+
|
|
78
|
+
data, err = json_loads_e('{"a": 1}')
|
|
79
|
+
assert err is None
|
|
80
|
+
assert data = {"a": 1}
|
|
81
|
+
|
|
82
|
+
data, err = json_loads_e('{"a": }')
|
|
83
|
+
assert err is not None
|
|
84
|
+
assert data is None
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Get `err` Traceback Stack
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import json
|
|
91
|
+
from with_err import with_err, get_err_strs
|
|
92
|
+
|
|
93
|
+
def json_loads_e(a: str | bytes | bytearray):
|
|
94
|
+
return json.loads(a)
|
|
95
|
+
|
|
96
|
+
_data, err = json_loads_e('{"a": 1}')
|
|
97
|
+
err_stack = get_err_strs(err)
|
|
98
|
+
assert err is None
|
|
99
|
+
assert err_stack == []
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import json
|
|
104
|
+
import re
|
|
105
|
+
from with_err import with_err, get_err_strs
|
|
106
|
+
|
|
107
|
+
def json_loads_e(a: str | bytes | bytearray):
|
|
108
|
+
return json.loads(a)
|
|
109
|
+
|
|
110
|
+
_data, err = json_loads_e('{"a": }')
|
|
111
|
+
err_stack = get_err_strs(err)
|
|
112
|
+
err_str = '\n'.join(err_stack)
|
|
113
|
+
assert err is not None
|
|
114
|
+
assert len(err_stack) > 0
|
|
115
|
+
assert re.search(r'json/__init__.py", line \d+, in loads', err_str)
|
|
116
|
+
assert re.search(r', line \d+, in json_loads_e', err_str)
|
|
117
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
with_err/__init__.py,sha256=430ZmzuYzLEyGMGFFI-U21DuPG76yvo-8IwKQAPGY_E,137
|
|
2
|
+
with_err/utils.py,sha256=39OxLBB-rUOYgCrm9nmzyhsOV4AVhTusATC-dXM3NXE,284
|
|
3
|
+
with_err/with_err.py,sha256=xyLbgmhUewopFQVUFX5H_5V3jr1kSXmJyiczikJPcdY,2450
|
|
4
|
+
with_err-1.0.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
|
|
5
|
+
with_err-1.0.0.dist-info/entry_points.txt,sha256=i2oN8Ae25gU-XoxtDndkB7we3AiSvh8-LPe0e6_tgrM,19
|
|
6
|
+
with_err-1.0.0.dist-info/METADATA,sha256=fljLZz_FK4rU2G3NIe-DdkKiyGujOqkdpnrhnGppDmE,2477
|
|
7
|
+
with_err-1.0.0.dist-info/RECORD,,
|