diff --git a/pyproject.toml b/pyproject.toml index 33d2ceb..4b93ae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "danom" -version = "0.9.0" +version = "0.10.0" description = "Functional streams and monads" readme = "README.md" license = "MIT" diff --git a/src/danom/_result.py b/src/danom/_result.py index d566b33..f4737ef 100644 --- a/src/danom/_result.py +++ b/src/danom/_result.py @@ -12,6 +12,7 @@ ) import attrs +from attrs.validators import instance_of T_co = TypeVar("T_co", covariant=True) U_co = TypeVar("U_co", covariant=True) @@ -181,7 +182,10 @@ def unwrap(self) -> T_co: @attrs.define(frozen=True) class Err(Result): error: Any = attrs.field(default=None) - input_args: tuple[()] | SafeArgs | SafeMethodArgs = attrs.field(default=(), repr=False) + input_args: tuple[()] | SafeArgs | SafeMethodArgs = attrs.field( + default=(), validator=instance_of(tuple), repr=False + ) + traceback: str = attrs.field(default="", validator=instance_of(str)) details: list[dict[str, Any]] = attrs.field(factory=list, init=False, repr=False) def __attrs_post_init__(self) -> None: diff --git a/src/danom/_safe.py b/src/danom/_safe.py index 50b348b..1497340 100644 --- a/src/danom/_safe.py +++ b/src/danom/_safe.py @@ -1,4 +1,5 @@ import functools +import traceback from collections.abc import Callable from typing import ParamSpec @@ -26,7 +27,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[U, E]: try: return Ok(func(*args, **kwargs)) except Exception as e: # noqa: BLE001 - return Err(input_args=(args, kwargs), error=e) + return Err(error=e, input_args=(args, kwargs), traceback=traceback.format_exc()) return wrapper @@ -54,6 +55,6 @@ def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> Result[U, E]: # noqa: A try: return Ok(func(self, *args, **kwargs)) except Exception as e: # noqa: BLE001 - return Err(input_args=(self, args, kwargs), error=e) + return Err(error=e, input_args=(self, args, kwargs), traceback=traceback.format_exc()) return wrapper diff --git a/tests/conftest.py b/tests/conftest.py index 9883f5e..1a20313 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import asyncio from multiprocessing.managers import ListProxy from pathlib import Path -from typing import Any, Self +from typing import Any, Never, Self from src.danom import safe, safe_method from src.danom._result import Err, Ok, Result @@ -80,6 +80,11 @@ def safe_get_error_type(exception: Exception) -> str: return exception.__class__.__name__ +@safe +def div_zero(x: int) -> Never: + return x / 0 + + class Adder: def __init__(self) -> None: self.result = 0 diff --git a/tests/test_safe.py b/tests/test_safe.py index ffbe6ec..aec4237 100644 --- a/tests/test_safe.py +++ b/tests/test_safe.py @@ -2,7 +2,14 @@ import pytest -from tests.conftest import Adder, safe_add, safe_get_error_type, safe_raise_type_error +from tests.conftest import ( + REPO_ROOT, + Adder, + div_zero, + safe_add, + safe_get_error_type, + safe_raise_type_error, +) def test_valid_safe_pipeline(): @@ -50,3 +57,14 @@ def test_invalid_safe_method_pipeline(): assert not res.is_ok() with pytest.raises(ValueError): res.unwrap() + + +def test_traceback(): + err = div_zero() + assert err.traceback.replace(str(REPO_ROOT), ".").splitlines() == [ + "Traceback (most recent call last):", + ' File "./src/danom/_safe.py", line 28, in wrapper', + " return Ok(func(*args, **kwargs))", + " ^^^^^^^^^^^^^^^^^^^^^", + "TypeError: div_zero() missing 1 required positional argument: 'x'", + ] diff --git a/uv.lock b/uv.lock index 88f1a8d..66172c2 100644 --- a/uv.lock +++ b/uv.lock @@ -273,7 +273,7 @@ wheels = [ [[package]] name = "danom" -version = "0.9.0" +version = "0.10.0" source = { editable = "." } dependencies = [ { name = "attrs" },