diff --git a/pyproject.toml b/pyproject.toml index 056daf6..a183e5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "danom" -version = "0.10.1" +version = "0.10.2" description = "Functional streams and monads" readme = "README.md" license = "MIT" diff --git a/src/danom/_result.py b/src/danom/_result.py index f4737ef..dcbfe86 100644 --- a/src/danom/_result.py +++ b/src/danom/_result.py @@ -21,8 +21,7 @@ P = ParamSpec("P") Mappable = Callable[P, U_co] -ResultReturnType = TypeVar("ResultReturnType", bound="Result[U_co, E_co]") -Bindable = Callable[P, ResultReturnType] +Bindable = Callable[P, "Result[U_co, E_co]"] @attrs.define(frozen=True) @@ -68,7 +67,7 @@ def is_ok(self) -> bool: ... @abstractmethod - def map(self, func: Mappable, **kwargs: P.kwargs) -> ResultReturnType: + def map(self, func: Mappable, **kwargs: P.kwargs) -> Result[U_co, E_co]: """Pipe a pure function and wrap the return value with `Ok`. Given an `Err` will return self. @@ -82,7 +81,7 @@ def map(self, func: Mappable, **kwargs: P.kwargs) -> ResultReturnType: ... @abstractmethod - def map_err(self, func: Mappable, **kwargs: P.kwargs) -> ResultReturnType: + def map_err(self, func: Mappable, **kwargs: P.kwargs) -> Result[U_co, E_co]: """Pipe a pure function and wrap the return value with `Err`. Given an `Ok` will return self. @@ -96,7 +95,7 @@ def map_err(self, func: Mappable, **kwargs: P.kwargs) -> ResultReturnType: ... @abstractmethod - def and_then(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType: + def and_then(self, func: Bindable, **kwargs: P.kwargs) -> Result[U_co, E_co]: """Pipe another function that returns a monad. For `Err` will return original error. .. code-block:: python @@ -111,7 +110,7 @@ def and_then(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType: ... @abstractmethod - def or_else(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType: + def or_else(self, func: Bindable, **kwargs: P.kwargs) -> Result[U_co, E_co]: """Pipe a function that returns a monad to recover from an `Err`. For `Ok` will return original `Result`. .. code-block:: python @@ -165,7 +164,7 @@ def map(self, func: Mappable, **kwargs: P.kwargs) -> Ok[U_co]: def map_err(self, func: Mappable, **kwargs: P.kwargs) -> Ok[U_co]: # noqa: ARG002 return self - def and_then(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType: + def and_then(self, func: Bindable, **kwargs: P.kwargs) -> Result[U_co, E_co]: return func(self.inner, **kwargs) def or_else(self, func: Bindable, **kwargs: P.kwargs) -> Ok[T_co]: # noqa: ARG002 @@ -220,7 +219,7 @@ def map_err(self, func: Mappable, **kwargs: P.kwargs) -> Err[F_co]: def and_then(self, func: Bindable, **kwargs: P.kwargs) -> Err[E_co]: # noqa: ARG002 return self - def or_else(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType: + def or_else(self, func: Bindable, **kwargs: P.kwargs) -> Result[U_co, E_co]: return func(self.error, **kwargs) def unwrap(self) -> None: diff --git a/src/danom/_safe.py b/src/danom/_safe.py index f968e97..aeefd98 100644 --- a/src/danom/_safe.py +++ b/src/danom/_safe.py @@ -11,7 +11,7 @@ E = TypeVar("E") -def safe[**P, U](func: Callable[P, U]) -> Callable[P, Result[U, E]]: +def safe(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 @@ -35,9 +35,9 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[U, E]: return wrapper -def safe_method[T, **P, U]( +def safe_method( func: Callable[Concatenate[T, P], U], -) -> Callable[Concatenate[T, P], Result[U, E]]: +) -> 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. .. code-block:: python @@ -56,7 +56,7 @@ def add_one(self, a: int) -> int: """ @functools.wraps(func) - def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> Result[U, E]: + 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 diff --git a/src/danom/_stream.py b/src/danom/_stream.py index 708c9c4..e8e8285 100644 --- a/src/danom/_stream.py +++ b/src/danom/_stream.py @@ -66,9 +66,12 @@ def par_collect(self, workers: int = 4, *, use_threads: bool = False) -> tuple[U @abstractmethod async def async_collect(self) -> Awaitable[tuple[U, ...]]: ... + def __bool__(self) -> bool: + return bool(self.seq) + @attrs.define(frozen=True) -class Stream(_BaseStream): +class Stream[Type](_BaseStream): """An immutable lazy iterator with functional operations. Why bother? diff --git a/tests/test_stream.py b/tests/test_stream.py index c395c46..da1ce24 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -2,6 +2,8 @@ from pathlib import Path import pytest +from hypothesis import given +from hypothesis import strategies as st from src.danom import Stream from tests.conftest import ( @@ -173,3 +175,16 @@ async def test_async_tap(): ) assert sorted(val_logger.values) == [0, 1, 2, 3] assert sorted(val_logger_2.values) == [0, 1, 2, 3] + + +@given( + st.one_of( + st.tuples(st.lists(st.integers(), min_size=1), st.just(True)), + st.tuples(st.lists(st.integers(), max_size=0), st.just(False)), + st.tuples(st.dictionaries(st.characters(), st.integers(), min_size=1), st.just(True)), + st.tuples(st.dictionaries(st.characters(), st.integers(), max_size=0), st.just(False)), + ) +) +def test_stream_bool(args): + seq, expected_result = args + assert bool(Stream.from_iterable(seq)) == expected_result diff --git a/uv.lock b/uv.lock index 1f1f27b..21eabf1 100644 --- a/uv.lock +++ b/uv.lock @@ -317,7 +317,7 @@ wheels = [ [[package]] name = "danom" -version = "0.10.1" +version = "0.10.2" source = { editable = "." } dependencies = [ { name = "attrs" },