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
4 changes: 2 additions & 2 deletions .github/workflows/ci_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0

Expand Down Expand Up @@ -73,10 +73,10 @@
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
Expand Down
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.10.3"
version = "0.11.0"
description = "Functional streams and monads"
readme = "README.md"
license = "MIT"
Expand Down
31 changes: 29 additions & 2 deletions src/danom/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from typing import (
Any,
Concatenate,
Generic,
Literal,
Never,
ParamSpec,
Expand All @@ -28,7 +27,7 @@


@attrs.define(frozen=True)
class Result(ABC, Generic[T_co, E_co]):
class Result[T_co, E_co: object](ABC):
"""`Result` monad. Consists of `Ok` and `Err` for successful and failed operations respectively.
Each monad is a frozen instance to prevent further mutation.
"""
Expand Down Expand Up @@ -150,6 +149,34 @@ def unwrap(self) -> T_co:
"""
...

@staticmethod
def result_is_ok(result: Result[T_co, E_co]) -> bool:
"""Check whether the monad is ok. Allows for ``filter`` or ``partition`` in a ``Stream`` without needing a lambda or custom function.

.. code-block:: python

from danom import Stream, Result

Stream.from_iterable([Ok(), Ok(), Err()]).filter(Result.result_is_ok).collect() == (Ok(), Ok())

"""
return result.is_ok()

@staticmethod
def result_unwrap(result: Result[T_co, E_co]) -> T_co:
"""Unwrap the `Ok` monad and get the inner value.
Unwrap the `Err` monad will raise the inner error.

.. code-block:: python

from danom import Stream, Result

oks, errs = Stream.from_iterable([Ok(1), Ok(2), Err()]).partition(Result.result_is_ok)
oks.map(Result.result_unwrap).collect == (1, 2)

"""
return result.unwrap()


@attrs.define(frozen=True, hash=True)
class Ok(Result[T_co, Never]):
Expand Down
12 changes: 8 additions & 4 deletions src/danom/_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
E = TypeVar("E")


def safe(func: Callable[P, U]) -> Callable[P, Result[U, Exception]]:
def safe[**P, U](func: Callable[P, U]) -> Callable[P, Result[U, Exception]]:
"""Decorator for functions that wraps the function in a try except returns `Ok` on success else `Err`.

.. code-block:: python
Expand All @@ -30,12 +30,14 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[U, Exception]:
try:
return Ok(func(*args, **kwargs))
except Exception as e: # noqa: BLE001
return Err(error=e, input_args=(args, kwargs), traceback=traceback.format_exc())
return Err(
error=e, input_args=(args, kwargs), traceback=traceback.format_exc()
) # ty: ignore[invalid-return-type]

return wrapper


def safe_method(
def safe_method[T, **P, U](
func: Callable[Concatenate[T, P], U],
) -> Callable[Concatenate[T, P], Result[U, Exception]]:
"""The same as `safe` except it forwards on the `self` of the class instance to the wrapped function.
Expand All @@ -60,6 +62,8 @@ def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> Result[U, Exception]:
try:
return Ok(func(self, *args, **kwargs))
except Exception as e: # noqa: BLE001
return Err(error=e, input_args=(self, args, kwargs), traceback=traceback.format_exc())
return Err(
error=e, input_args=(self, args, kwargs), traceback=traceback.format_exc()
) # ty: ignore[invalid-return-type]

return wrapper
39 changes: 39 additions & 0 deletions tests/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,42 @@ def test_raises_not_implemented(cls):
def test_err_details(err, expected_details):
monad = Err(error=err)
assert monad.details == expected_details


@pytest.mark.parametrize(
("monad", "expected_result"),
[
pytest.param(Ok(None), True),
pytest.param(Err(), False),
],
)
def test_staticmethod_result_is_ok(monad, expected_result):
assert Result.result_is_ok(monad) == expected_result


@pytest.mark.parametrize(
("monad", "expected_result", "expected_context"),
[
pytest.param(Ok(None), None, nullcontext()),
pytest.param(Ok(0), 0, nullcontext()),
pytest.param(Ok("ok"), "ok", nullcontext()),
pytest.param(
Err(error=TypeError("should raise this")),
None,
pytest.raises(TypeError),
),
pytest.param(
Err(error=ValueError("should raise this")),
None,
pytest.raises(ValueError),
),
pytest.param(
Err("some other err representation"),
None,
pytest.raises(ValueError),
),
],
)
def test_staticmethod_result_unwrap(monad, expected_result, expected_context):
with expected_context:
assert Result.result_unwrap(monad) == expected_result
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.

Loading