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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ envs/
scrap/
.coverage
.DS_Store
.benchmarks/
.codspeed/
248 changes: 171 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,114 +4,102 @@

# API Reference

## Ok

Frozen instance of an Ok monad used to wrap successful operations.

### `Ok.and_then`
```python
Ok.and_then(self, func: collections.abc.Callable[[~T], danom._result.Result], **kwargs: dict) -> danom._result.Result
```
Pipe another function that returns a monad.
## Stream

```python
>>> Ok(1).and_then(add_one) == Ok(2)
>>> Ok(1).and_then(raise_err) == Err(error=TypeError())
```
An immutable lazy iterator with functional operations.

#### Why bother?
Readability counts, abstracting common operations helps reduce cognitive complexity when reading code.

### `Ok.is_ok`
```python
Ok.is_ok(self) -> Literal[True]
```
Returns True if the result type is Ok.
#### Comparison
Take this imperative pipeline of operations, it iterates once over the :

```python
>>> Ok().is_ok() == True
```


### `Ok.match`
```python
Ok.match(self, if_ok_func: collections.abc.Callable[[~T], danom._result.Result], _if_err_func: collections.abc.Callable[[~T], danom._result.Result]) -> danom._result.Result
```
Map Ok func to Ok and Err func to Err

```python
>>> 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')
>>> res = []
...
>>> for x in range(1_000_000):
... item = triple(x)
...
... if not is_gt_ten(item):
... continue
...
... item = min_two(item)
...
... if not is_even_num(item):
... continue
...
... item = square(item)
...
... if not is_lt_400(item):
... continue
...
... res.append(item)
>>> [100, 256]
```
number of tokens: `90`

number of keywords: `11`

### `Ok.unwrap`
```python
Ok.unwrap(self) -> ~T
```
Unwrap the Ok monad and get the inner value.
keyword breakdown: `{'for': 1, 'in': 1, 'if': 3, 'not': 3, 'continue': 3}`

After a bit of experience with python you might use list comprehensions, however this is arguably _less_ clear and iterates multiple times over the same data
```python
>>> Ok().unwrap() == None
>>> Ok(1).unwrap() == 1
>>> Ok("ok").unwrap() == 'ok'
>>> mul_three = [triple(x) for x in range(1_000_000)]
>>> gt_ten = [x for x in mul_three if is_gt_ten(x)]
>>> sub_two = [min_two(x) for x in gt_ten]
>>> is_even = [x for x in sub_two if is_even_num(x)]
>>> squared = [square(x) for x in is_even]
>>> lt_400 = [x for x in squared if is_lt_400(x)]
>>> [100, 256]
```
number of tokens: `92`

number of keywords: `15`

## Err

Frozen instance of an Err monad used to wrap failed operations.
keyword breakdown: `{'for': 6, 'in': 6, 'if': 3}`

### `Err.and_then`
```python
Err.and_then(self, _: 'Callable[[T], Result]', **_kwargs: 'dict') -> 'Self'
```
Pipe another function that returns a monad. For Err will return original error.
This still has a lot of tokens that the developer has to read to understand the code. The extra keywords add noise that cloud the actual transformations.

Using a `Stream` results in this:
```python
>>> Err(error=TypeError()).and_then(add_one) == Err(error=TypeError())
>>> Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError())
>>> (
... Stream.from_iterable(range(1_000_000))
... .map(triple)
... .filter(is_gt_ten)
... .map(min_two)
... .filter(is_even_num)
... .map(square)
... .filter(is_lt_400)
... .collect()
... )
>>> (100, 256)
```
number of tokens: `60`

number of keywords: `0`

### `Err.is_ok`
```python
Err.is_ok(self) -> 'Literal[False]'
```
Returns False if the result type is Err.
keyword breakdown: `{}`

```python
Err().is_ok() == False
```
The business logic is arguably much clearer like this.


### `Err.match`
### `Stream.async_collect`
```python
Err.match(self, _if_ok_func: 'Callable[[T], Result]', if_err_func: 'Callable[[T], Result]') -> 'Result'
Stream.async_collect(self) -> 'tuple'
```
Map Ok func to Ok and Err func to Err
Async version of collect. Note that all functions in the stream should be `Awaitable`.

```python
>>> 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')
>>> Stream.from_iterable(file_paths).map(async_read_files).async_collect()
```


### `Err.unwrap`
```python
Err.unwrap(self) -> 'None'
```
Unwrap the Err monad will raise the inner error.
If there are no operations in the `Stream` then this will act as a normal collect.

```python
>>> Err(error=TypeError()).unwrap() raise TypeError(...)
>>> Stream.from_iterable(file_paths).async_collect()
```


## Stream

A lazy iterator with functional operations.

### `Stream.collect`
```python
Stream.collect(self) -> 'tuple'
Expand Down Expand Up @@ -222,6 +210,8 @@ If False the processing will use `ProcessPoolExecutor` else it will use `ThreadP
>>> stream.par_collect(use_threads=True) == (1, 2, 3, 4)
```

Note that all operations should be pickle-able, for that reason `Stream` does not support lambdas or closures.


### `Stream.partition`
```python
Expand All @@ -244,11 +234,115 @@ As `partition` triggers an action, the parameters will be forwarded to the `par_
```


## Ok

Frozen instance of an Ok monad used to wrap successful operations.

### `Ok.and_then`
```python
Ok.and_then(self, func: collections.abc.Callable[[~T], danom._result.Result], **kwargs: dict) -> danom._result.Result
```
Pipe another function that returns a monad.

```python
>>> Ok(1).and_then(add_one) == Ok(2)
>>> Ok(1).and_then(raise_err) == Err(error=TypeError())
```


### `Ok.is_ok`
```python
Ok.is_ok(self) -> Literal[True]
```
Returns True if the result type is Ok.

```python
>>> Ok().is_ok() == True
```


### `Ok.match`
```python
Ok.match(self, if_ok_func: collections.abc.Callable[[~T], danom._result.Result], _if_err_func: collections.abc.Callable[[~T], danom._result.Result]) -> danom._result.Result
```
Map Ok func to Ok and Err func to Err

```python
>>> 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')
```


### `Ok.unwrap`
```python
Ok.unwrap(self) -> ~T
```
Unwrap the Ok monad and get the inner value.

```python
>>> Ok().unwrap() == None
>>> Ok(1).unwrap() == 1
>>> Ok("ok").unwrap() == 'ok'
```


## Err

Frozen instance of an Err monad used to wrap failed operations.

### `Err.and_then`
```python
Err.and_then(self, _: 'Callable[[T], Result]', **_kwargs: 'dict') -> 'Self'
```
Pipe another function that returns a monad. For Err will return original error.

```python
>>> Err(error=TypeError()).and_then(add_one) == Err(error=TypeError())
>>> Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError())
```


### `Err.is_ok`
```python
Err.is_ok(self) -> 'Literal[False]'
```
Returns False if the result type is Err.

```python
Err().is_ok() == False
```


### `Err.match`
```python
Err.match(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
>>> 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.unwrap`
```python
Err.unwrap(self) -> 'None'
```
Unwrap the Err monad will raise the inner error.

```python
>>> Err(error=TypeError()).unwrap() raise TypeError(...)
```


## safe

### `safe`
```python
safe(func: collections.abc.Callable[~P, ~T]) -> collections.abc.Callable[~P, danom._result.Result]
safe(func: collections.abc.Callable[[T], U]) -> collections.abc.Callable[[T], danom._result.Result]
```
Decorator for functions that wraps the function in a try except returns `Ok` on success else `Err`.

Expand All @@ -265,7 +359,7 @@ Decorator for functions that wraps the function in a try except returns `Ok` on

### `safe_method`
```python
safe_method(func: collections.abc.Callable[~P, ~T]) -> collections.abc.Callable[~P, danom._result.Result]
safe_method(func: collections.abc.Callable[[T], U]) -> collections.abc.Callable[[T], danom._result.Result]
```
The same as `safe` except it forwards on the `self` of the class instance to the wrapped function.

Expand Down
2 changes: 1 addition & 1 deletion coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 8 additions & 3 deletions dev_tools/update_cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@

def update_cov_badge(root: str) -> int:
cov_report = json.loads(Path(f"{root}/coverage.json").read_text())
new_pct = cov_report["totals"]["percent_covered"]
new_pct = to_2dp_float_str(cov_report["totals"]["percent_covered"])

curr_badge = Path(f"{root}/coverage.svg").read_text()
curr_pct = float(
curr_pct = to_2dp_float_str(
re.findall(r'<text x="90" y="14">([0-9]+(?:\.[0-9]+)?)%</text>', curr_badge)[0]
)

Expand All @@ -40,6 +40,7 @@ def update_cov_badge(root: str) -> int:


def make_badge(badge_str: str, pct: int) -> str:
pct = float(pct)
colour = (
"red"
if pct < 50 # noqa: PLR2004
Expand All @@ -51,7 +52,11 @@ def make_badge(badge_str: str, pct: int) -> str:
if pct < 90 # noqa: PLR2004
else "green"
)
return badge_str.format(colour=colour, pct=pct)
return badge_str.format(colour=colour, pct=f"{pct:.2f}")


def to_2dp_float_str(pct: float) -> str:
return f"{float(pct):.2f}"


if __name__ == "__main__":
Expand Down
10 changes: 7 additions & 3 deletions dev_tools/update_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ class ReadmeDoc:
doc: str

def to_readme(self) -> str:
docs = "\n".join([line.strip() for line in self.doc.splitlines()])
docs = strip_doc(self.doc)
return "\n".join([f"### `{self.name}`", f"```python\n{self.name}{self.sig}\n```", docs])


def create_readme_lines() -> str:
readme_lines = []

for ent in [Ok, Err, Stream]:
for ent in [Stream, Ok, Err]:
readme_lines.append(f"## {ent.__name__}")
readme_lines.append(ent.__doc__)
readme_lines.append(strip_doc(ent.__doc__))
readme_docs = [
ReadmeDoc(f"{ent.__name__}.{k}", inspect.signature(v), v.__doc__)
for k, v in inspect.getmembers(ent, inspect.isroutine)
Expand All @@ -62,5 +62,9 @@ def update_readme(new_docs: str, readme_path: str = "./README.md") -> None:
return 0


def strip_doc(doc: str) -> str:
return "\n".join([line.strip() for line in doc.splitlines()])


if __name__ == "__main__":
sys.exit(update_readme(create_readme_lines()))
Loading