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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,5 +286,7 @@ Alternatively the map method can be used to return a new type instance with the
├── pyproject.toml
├── ruff.toml
└── uv.lock

(generated with repo-mapper-rs)
::
```
2 changes: 2 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ deploy:
fi
@cd docs-build && git worktree add -f html gh-pages || true

doctest:
@uv run sphinx-build -b doctest ./source ./docs-build/doctest

local:
@uv run sphinx-apidoc -o source src/danom/ --separate ; uv run sphinx-build source docs-build/html
Expand Down
1 change: 1 addition & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.coverage",
"sphinx.ext.doctest",
"sphinx.ext.napoleon",
"sphinx.ext.githubpages",
]
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "danom"
version = "0.8.2"
version = "0.9.0"
description = "Functional streams and monads"
readme = "README.md"
license = "MIT"
Expand Down Expand Up @@ -29,7 +29,7 @@ dev = [
"pytest-asyncio>=1.3.0",
"pytest-codspeed>=4.2.0",
"pytest-cov>=7.0.0",
"repo-mapper-rs>=0.3.0",
"repo-mapper-rs>=0.4.0",
"ruff>=0.14.6",
"sphinx>=9.0.4",
"ty>=0.0.8",
Expand Down
26 changes: 16 additions & 10 deletions src/danom/_new_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ def new_type( # noqa: ANN202
):
"""Create a NewType based on another type.

.. code-block:: python
.. doctest::

from danom import new_type
>>> from danom import new_type

def is_positive(value):
return value >= 0
>>> def is_positive[T](value: T) -> bool:
... return value >= 0

ValidBalance = new_type("ValidBalance", float, validators=[is_positive])
ValidBalance("20") == ValidBalance(inner=20.0)
>>> ValidBalance = new_type("ValidBalance", float, validators=[is_positive])
>>> ValidBalance(20.0) == ValidBalance(inner=20.0)
True

Unlike an inherited class, the type will not return `True` for an isinstance check.

Expand Down Expand Up @@ -99,7 +100,7 @@ def _callables_to_kwargs(
def _validate_bool_func[T](
bool_fn: Callable[..., bool],
) -> Callable[[attrs.AttrsInstance, attrs.Attribute, T], None]:
if not isinstance(bool_fn, Callable):
if not callable(bool_fn):
raise TypeError("provided boolean function must be callable")

@wraps(bool_fn)
Expand All @@ -118,6 +119,11 @@ def wrapper(_instance: attrs.AttrsInstance, attribute: attrs.Attribute, value: T
def _to_list(value: C | Sequence[C] | None) -> list[C]:
if value is None:
return []
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
return list(value)
return [value]

if callable(value):
return [value]

if isinstance(value, Sequence) and not all(callable(fn) for fn in value):
raise TypeError(f"Given items are not all callable: {value = }")

return list(value)
101 changes: 69 additions & 32 deletions src/danom/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
T_co = TypeVar("T_co", covariant=True)
U_co = TypeVar("U_co", covariant=True)
E_co = TypeVar("E_co", bound=object, covariant=True)
F_co = TypeVar("F_co", bound=object, covariant=True)
P = ParamSpec("P")

Mappable = Callable[P, U_co]
Expand All @@ -33,13 +34,18 @@ class Result(ABC):
def unit(cls, inner: T_co) -> Ok[T_co]:
"""Unit method. Given an item of type `T_co` return `Ok(T_co)`

.. code-block:: python
.. doctest::

>>> from danom import Err, Ok, Result

>>> Result.unit(0) == Ok(inner=0)
True

from danom import Err, Ok, Result
>>> Ok.unit(0) == Ok(inner=0)
True

Result.unit(0) == Ok(inner=0)
Ok.unit(0) == Ok(inner=0)
Err.unit(0) == Ok(inner=0)
>>> Err.unit(0) == Ok(inner=0)
True
"""
return Ok(inner)

Expand All @@ -48,12 +54,15 @@ def is_ok(self) -> bool:
"""Returns `True` if the result type is `Ok`.
Returns `False` if the result type is `Err`.

.. code-block:: python
.. doctest::

from danom import Err, Ok
>>> from danom import Err, Ok

Ok().is_ok() == True
Err().is_ok() == False
>>> Ok().is_ok() == True
True

>>> Err().is_ok() == False
True
"""
...

Expand All @@ -71,6 +80,20 @@ def map(self, func: Mappable, **kwargs: P.kwargs) -> ResultReturnType:
"""
...

@abstractmethod
def map_err(self, func: Mappable, **kwargs: P.kwargs) -> ResultReturnType:
"""Pipe a pure function and wrap the return value with `Err`.
Given an `Ok` will return self.

.. code-block:: python

from danom import Err, Ok

Err(error=TypeError()).map_err(type_err_to_value_err) == Err(error=ValueError())
Ok(1).map(type_err_to_value_err) == Ok(1)
"""
...

@abstractmethod
def and_then(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType:
"""Pipe another function that returns a monad. For `Err` will return original error.
Expand All @@ -87,32 +110,40 @@ def and_then(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType:
...

@abstractmethod
def unwrap(self) -> T_co:
"""Unwrap the `Ok` monad and get the inner value.
Unwrap the `Err` monad will raise the inner error.
def or_else(self, func: Bindable, **kwargs: P.kwargs) -> ResultReturnType:
"""Pipe a function that returns a monad to recover from an `Err`. For `Ok` will return original `Result`.

.. code-block:: python

from danom import Err, Ok

Ok().unwrap() == None
Ok(1).unwrap() == 1
Ok("ok").unwrap() == 'ok'
Err(error=TypeError()).unwrap() raise TypeError(...)
Ok(1).or_else(replace_err_with_zero) == Ok(1)
Err(error=TypeError()).or_else(replace_err_with_zero) == Ok(0)
"""
...

@abstractmethod
def match(self, if_ok_func: Mappable, if_err_func: Mappable) -> ResultReturnType:
"""Map `ok_func` to `Ok` and `err_func` to `Err`
def unwrap(self) -> T_co:
"""Unwrap the `Ok` monad and get the inner value.
Unwrap the `Err` monad will raise the inner error.

.. code-block:: python
.. doctest::

from danom import Err, Ok
>>> from danom import Err, Ok

>>> Ok().unwrap() == None
True

>>> Ok(1).unwrap() == 1
True

>>> Ok("ok").unwrap() == 'ok'
True

Ok(1).match(add_one, mock_get_error_type) == Ok(inner=2)
Ok("ok").match(double, mock_get_error_type) == Ok(inner='okok')
Err(error=TypeError()).match(double, mock_get_error_type) == Ok(inner='TypeError')
>>> Err(error=TypeError()).unwrap()
Traceback (most recent call last):
...
TypeError:
"""
...

Expand All @@ -121,7 +152,7 @@ def __class_getitem__(cls, _params: tuple) -> Self:


@attrs.define(frozen=True, hash=True)
class Ok[T_co](Result):
class Ok(Result):
inner: Any = attrs.field(default=None)

def is_ok(self) -> Literal[True]:
Expand All @@ -130,23 +161,26 @@ def is_ok(self) -> Literal[True]:
def map(self, func: Mappable, **kwargs: P.kwargs) -> Ok[U_co]:
return Ok(func(self.inner, **kwargs))

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:
return func(self.inner, **kwargs)

def or_else(self, func: Bindable, **kwargs: P.kwargs) -> Ok[T_co]: # noqa: ARG002
return self

def unwrap(self) -> T_co:
return self.inner

def match(self, if_ok_func: Mappable, if_err_func: Mappable) -> ResultReturnType: # noqa: ARG002
return if_ok_func(self.inner)


SafeArgs = tuple[tuple[Any, ...], dict[str, Any]]
SafeMethodArgs = tuple[object, tuple[Any, ...], dict[str, Any]]


@attrs.define(frozen=True)
class Err[E_co](Result):
error: E_co | Exception = attrs.field(default=None)
class Err(Result):
error: Any = attrs.field(default=None)
input_args: tuple[()] | SafeArgs | SafeMethodArgs = attrs.field(default=(), repr=False)
details: list[dict[str, Any]] = attrs.field(factory=list, init=False, repr=False)

Expand Down Expand Up @@ -176,17 +210,20 @@ def is_ok(self) -> Literal[False]:
def map(self, func: Mappable, **kwargs: P.kwargs) -> Err[E_co]: # noqa: ARG002
return self

def map_err(self, func: Mappable, **kwargs: P.kwargs) -> Err[F_co]:
return Err(func(self.error, **kwargs))

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:
return func(self.error, **kwargs)

def unwrap(self) -> None:
if isinstance(self.error, Exception):
raise self.error
raise ValueError(f"Err does not have a caught error to raise: {self.error = }")

def match(self, if_ok_func: Mappable, if_err_func: Mappable) -> ResultReturnType: # noqa: ARG002
return if_err_func(self.error)

def __eq__(self, other: object) -> bool:
if not isinstance(other, Err):
return False
Expand Down
Loading