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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
6 changes: 5 additions & 1 deletion src/danom/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions src/danom/_safe.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import traceback
from collections.abc import Callable
from typing import ParamSpec

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion tests/test_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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'",
]
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.