diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 7970eaa..7ac68fa 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -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" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 600dd97..472f93b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -23,7 +23,7 @@ runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 25caa42..a68bf34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/danom/_result.py b/src/danom/_result.py index 2c22def..a157690 100644 --- a/src/danom/_result.py +++ b/src/danom/_result.py @@ -6,7 +6,6 @@ from typing import ( Any, Concatenate, - Generic, Literal, Never, ParamSpec, @@ -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. """ @@ -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]): diff --git a/src/danom/_safe.py b/src/danom/_safe.py index 20fb410..0b44cdb 100644 --- a/src/danom/_safe.py +++ b/src/danom/_safe.py @@ -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 @@ -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. @@ -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 diff --git a/tests/test_result.py b/tests/test_result.py index b2e31d6..93d37c4 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -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 diff --git a/uv.lock b/uv.lock index 0db4803..6245019 100644 --- a/uv.lock +++ b/uv.lock @@ -317,7 +317,7 @@ wheels = [ [[package]] name = "danom" -version = "0.10.3" +version = "0.11.0" source = { editable = "." } dependencies = [ { name = "attrs" },