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
162 changes: 104 additions & 58 deletions README.md

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions dev_tools/update_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import attrs

from danom import (
Err,
Ok,
Result,
Stream,
all_of,
any_of,
Expand All @@ -34,7 +33,7 @@ def to_readme(self) -> str:
def create_readme_lines() -> str:
readme_lines = []

for ent in [Stream, Ok, Err]:
for ent in [Stream, Result]:
readme_lines.append(f"## {ent.__name__}")
readme_lines.append(strip_doc(ent.__doc__))
readme_docs = [
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.7.0"
version = "0.8.0"
description = "Functional streams and monads"
readme = "README.md"
license = "MIT"
Expand Down
4 changes: 1 addition & 3 deletions src/danom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from danom._err import Err
from danom._new_type import new_type
from danom._ok import Ok
from danom._result import Result
from danom._result import Err, Ok, Result
from danom._safe import safe, safe_method
from danom._stream import Stream
from danom._utils import all_of, any_of, compose, identity, invert, none_of
Expand Down
86 changes: 0 additions & 86 deletions src/danom/_err.py

This file was deleted.

2 changes: 2 additions & 0 deletions src/danom/_new_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def new_type( # noqa: ANN202
"""Create a NewType based on another type.

```python
>>> from danom import new_type
>>> def is_positive(value):
... return value >= 0

Expand All @@ -35,6 +36,7 @@ def new_type( # noqa: ANN202
The methods of the given `base_type` will be forwarded to the specialised type.
Alternatively the map method can be used to return a new type instance with the transformation.
```python
>>> from danom import new_type
>>> def has_len(email: str) -> bool:
... return len(email) > 0

Expand Down
59 changes: 0 additions & 59 deletions src/danom/_ok.py

This file was deleted.

160 changes: 149 additions & 11 deletions src/danom/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,171 @@

from abc import ABC, abstractmethod
from collections.abc import Callable
from types import TracebackType
from typing import (
ParamSpec,
Any,
Literal,
Self,
TypeVar,
)

import attrs

T = TypeVar("T")
P = ParamSpec("P")


@attrs.define
class Result(ABC):
class Result[T, U](ABC):
"""`Result` monad. Consists of `Ok` and `Err` for successful and failed operations respectively.
Each monad is a frozen instance to prevent further mutation.
"""

@classmethod
def unit(cls, inner: T) -> Ok[T]:
"""Unit method. Given an item of type `T` return `Ok(T)`

```python
>>> from danom import Err, Ok, Result
>>> Result.unit(0) == Ok(inner=0)
>>> Ok.unit(0) == Ok(inner=0)
>>> Err.unit(0) == Ok(inner=0)
```
"""
return Ok(inner)

@abstractmethod
def is_ok(self) -> bool: ...
def is_ok(self) -> bool:
"""Returns `True` if the result type is `Ok`.
Returns `False` if the result type is `Err`.

```python
>>> from danom import Err, Ok
>>> Ok().is_ok() == True
>>> Err().is_ok() == False
```
"""
...

@abstractmethod
def and_then(self, func: Callable[[T], Result], **kwargs: dict) -> Result: ...
def map(self, func: Callable[[T], U], **kwargs: dict) -> Result[U]:
"""Pipe a pure function and wrap the return value with `Ok`.
Given an `Err` will return self.

```python
>>> from danom import Err, Ok
>>> Ok(1).map(add_one) == Ok(2)
>>> Err(error=TypeError()).map(add_one) == Err(error=TypeError())
```
"""
...

@abstractmethod
def unwrap(self) -> T: ...
def and_then(self, func: Callable[[T], Result[U]], **kwargs: dict) -> Result[U]:
"""Pipe another function that returns a monad. For `Err` will return original error.

```python
>>> from danom import Err, Ok
>>> Ok(1).and_then(add_one) == Ok(2)
>>> Ok(1).and_then(raise_err) == Err(error=TypeError())
>>> Err(error=TypeError()).and_then(add_one) == Err(error=TypeError())
>>> Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError())
```
"""
...

@abstractmethod
def unwrap(self) -> T:
"""Unwrap the `Ok` monad and get the inner value.
Unwrap the `Err` monad will raise the inner error.
```python
>>> from danom import Err, Ok
>>> Ok().unwrap() == None
>>> Ok(1).unwrap() == 1
>>> Ok("ok").unwrap() == 'ok'
>>> Err(error=TypeError()).unwrap() raise TypeError(...)
```
"""
...

@abstractmethod
def match(
self, if_ok_func: Callable[[T], Result], _if_err_func: Callable[[T], Result]
) -> Result: ...
self, if_ok_func: Callable[[T], Result], if_err_func: Callable[[T], Result]
) -> Result:
"""Map `ok_func` to `Ok` and `err_func` to `Err`

```python
>>> from danom import Err, Ok
>>> 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')
```
"""
...

def __class_getitem__(cls, _params: tuple) -> Self:
return cls


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

def is_ok(self) -> Literal[True]:
return True

def map(self, func: Callable[[T], U], **kwargs: dict) -> Result[U]:
return Ok(func(self.inner, **kwargs))

def and_then(self, func: Callable[[T], Result[U]], **kwargs: dict) -> Result[U]:
return func(self.inner, **kwargs)

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

def match(
self, if_ok_func: Callable[[T], Result], _if_err_func: Callable[[T], Result]
) -> Result:
return if_ok_func(self.inner)


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

def __attrs_post_init__(self) -> None:
if isinstance(self.error, Exception):
# little hack explained here: https://www.attrs.org/en/stable/init.html#post-init
object.__setattr__(self, "details", self._extract_details(self.error.__traceback__))

def _extract_details(self, tb: TracebackType | None) -> list[dict[str, Any]]:
trace_info = []
while tb:
frame = tb.tb_frame
trace_info.append(
{
"file": frame.f_code.co_filename,
"func": frame.f_code.co_name,
"line_no": tb.tb_lineno,
"locals": frame.f_locals,
},
)
tb = tb.tb_next
return trace_info

def is_ok(self) -> Literal[False]:
return False

def map(self, _: Callable[[T], U], **_kwargs: dict) -> Result[U]:
return self

def and_then(self, _: Callable[[T], Result[U]], **_kwargs: dict) -> Self:
return self

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: Callable[[T], Result], if_err_func: Callable[[T], Result]
) -> Result:
return if_err_func(self.error)
Loading