Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 88 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ json_loads_e = with_err(json.loads)

data, err = json_loads_e('{"a": 1}')
assert err is None
assert data = {"a": 1}
assert data == {"a": 1}

data, err = json_loads_e('{"a": }')
assert isinstance(err, json.decoder.JSONDecodeError)
Expand All @@ -44,7 +44,7 @@ def json_loads_e(a: str | bytes | bytearray):

data, err = json_loads_e('{"a": 1}')
assert err is None
assert data = {"a": 1}
assert data == {"a": 1}

data, err = json_loads_e('{"a": }')
assert isinstance(err, json.decoder.JSONDecodeError)
Expand All @@ -65,7 +65,7 @@ def json_loads_e(a: str | bytes | bytearray):

data, err = json_loads_e('{"a": 1}')
assert err is None
assert data = {"a": 1}
assert data == {"a": 1}

data, err = json_loads_e('{"a": }')
assert isinstance(err, json.decoder.JSONDecodeError)
Expand Down Expand Up @@ -96,7 +96,7 @@ json_loads_e = with_err(json.decoder.JSONDecodeError)(json.loads)

data, err = json_loads_e('{"a": 1}')
assert err is None
assert data = {"a": 1}
assert data == {"a": 1}

data, err = json_loads_e('{"a": }')
assert isinstance(err, json.decoder.JSONDecodeError)
Expand All @@ -113,13 +113,70 @@ json_loads_e = with_err()(json.loads)

data, err = json_loads_e('{"a": 1}')
assert err is None
assert data = {"a": 1}
assert data == {"a": 1}

data, err = json_loads_e('{"a": }')
assert isinstance(err, json.decoder.JSONDecodeError)
assert data is None
```

### Async Functions

```python
from with_err import with_err

@with_err
async def async_fetch_data(endpoint: str) -> dict[str, str]:
if endpoint == "bad":
raise ValueError("Failed to reach endpoint")
return {"status": "ok"}

async_fetch_data_e = with_err(async_fetch_data)
res, err = await async_fetch_data_e("bad")
assert isinstance(err, ValueError)
assert res is None
```

### Generators

```python
from with_err import with_err


@with_err
def my_stream():
yield 1
raise ValueError('invalid')

for idx, (each, err) in enumerate(my_stream()):
if idx == 0:
assert each == 1
assert err is None
else:
assert each is None
assert isinstance(err, ValueError)
```

### Async Generators

```python
from with_err import with_err


@with_err
async def my_async_stream():
yield 1
raise ValueError('invalid')

async for each, err in my_stream():
if each == 1:
assert each == 1
assert err is None
else:
assert each is None
assert isinstance(err, ValueError)
```

### Get `err` Traceback Stack

```python
Expand Down Expand Up @@ -148,7 +205,32 @@ err_stack = get_err_strs(err)
err_str = '\n'.join(err_stack)
assert isinstance(err, json.decoder.JSONDecodeError)
assert len(err_stack) > 0
assert 'json.decoder.JSONDecodeError: Expecting value:' in err_str
assert re.search(r', line \d+, in json_loads_e', err_str)
assert re.search(r'json/__init__.py", line \d+, in loads', err_str)
assert 'json.decoder.JSONDecodeError: Expecting value:' in err_str
```

### Raise `err`

```python
import json
import re
from with_err import with_err, get_err_strs, raise_err

def json_loads_e(a: str | bytes | bytearray):
return json.loads(a)

def gen_err():
data, err = json_loads_e('{"a": }')
return data, raise_err(err)

data, err = gen_err()
err_stack = get_err_strs(err)
err_str = '\n'.join(err_stack)
assert isinstance(err, json.decoder.JSONDecodeError)
assert len(err_stack) > 0
assert re.search(r', line \d+, in gen_err', err_str)
assert re.search(r', line \d+, in json_loads_e', err_str)
assert re.search(r'json/__init__.py", line \d+, in loads', err_str)
assert 'json.decoder.JSONDecodeError: Expecting value:' in err_str
```
6 changes: 4 additions & 2 deletions src/with_err/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from .raise_err import raise_err as raise_err
from .utils import get_err_strs as get_err_strs
from .with_err import with_err as with_err

all = [
__all__ = [
'get_err_strs',
'raise_err',
'with_err',
'get_err_strs'
]
57 changes: 57 additions & 0 deletions src/with_err/raise_err.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# https://chatgpt.com/c/6a8b3620-9cb0-83ea-baf6-1ab4a31e473b

import inspect
import types


def raise_err(err: Exception | None):
'''
`raise_err` does not raise the exception, but augments traceback with the caller's frame.
'''
if err is None:
return

parent_frame = _get_parent_frame()
if parent_frame is None:
return err

tb = err.__traceback__

combined_tb = types.TracebackType(
tb_next=tb,
tb_frame=parent_frame,
tb_lasti=parent_frame.f_lasti,
tb_lineno=parent_frame.f_lineno,
)

return err.with_traceback(combined_tb)


def _get_parent_frame():
frame = _try_get_currentframe()

# frame as _get_parent_frame.
if frame is None:
return

# frame.f_back as raise_err
if frame.f_back is None:
return

# framer.f_back.f_back as caller frame of raise_err.
return frame.f_back.f_back


def _try_get_currentframe():
frame = inspect.currentframe()
if frame is not None: # frame as _try_get_currentframe
return frame.f_back # f_back as _get_parent_frame

# XXX hack for nuitka.
try:
raise ValueError('none')
except ValueError:
frame = inspect.currentframe() # frame as _try_get_currentframe
if frame is None:
return None
return frame.f_back # f_back as _get_parent_frame
Loading