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
34 changes: 34 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CodSpeed

on:
pull_request:
branches:
- "main"
- "dev"
workflow_dispatch:

permissions:
contents: read
id-token: write

jobs:
codspeed:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Install the project
run: uv sync --dev

- name: Run benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: uv run pytest tests/test_benchmarks.py --codspeed
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# danom

[![PyPI Downloads](https://static.pepy.tech/personalized-badge/danom?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/danom) ![coverage](./coverage.svg)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/danom?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/danom) ![coverage](./coverage.svg) [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/second-ed/danom?utm_source=badge)

# API Reference

Expand Down Expand Up @@ -275,6 +275,7 @@ Alternatively the map method can be used to return a new type instance with the
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_api.py
│ ├── test_benchmarks.py
│ ├── test_monad_laws.py
│ ├── test_new_type.py
│ ├── test_result.py
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.11.2"
version = "0.11.3"
description = "Functional streams and monads"
readme = "README.md"
license = "MIT"
Expand Down
17 changes: 11 additions & 6 deletions src/danom/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Awaitable, Callable, Iterable, Sequence
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from copy import deepcopy
from enum import Enum, auto
from enum import Enum
from functools import reduce
from itertools import batched
from typing import ParamSpec, TypeVar, cast
Expand Down Expand Up @@ -312,10 +312,15 @@ def partition(
seq_tuple = self.par_collect(workers=workers, use_threads=use_threads)
else:
seq_tuple = self.collect()
return (
Stream(seq=tuple(x for x in seq_tuple if fn(x))),
Stream(seq=tuple(x for x in seq_tuple if not fn(x))),
)

pos, neg = [], []

for x in seq_tuple:
if fn(x):
pos.append(x)
else:
neg.append(x)
return (Stream.from_iterable(pos), Stream.from_iterable(neg))

def fold(
self, initial: T, fn: Callable[[T, U], T], *, workers: int = 1, use_threads: bool = False
Expand Down Expand Up @@ -442,7 +447,7 @@ async def async_collect(self) -> Awaitable[tuple[U, ...]]:


class _Nothing(Enum):
NOTHING = auto()
NOTHING = 0


PlannedOps = tuple[str, StreamFn]
Expand Down
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,42 @@
REPO_ROOT = Path(__file__).parents[1]


def is_positive(x: float) -> bool:
return x > 0


def lt_100(x: float) -> bool:
return x < 100 # noqa: PLR2004


def add[T: (str, float, int)](a: T, b: T) -> T:
return a + b # ty: ignore[unsupported-operator]


def triple(x: float) -> float:
return x * 3


def is_gt_ten(x: float) -> float:
return x > 10 # noqa: PLR2004


def min_two(x: float) -> float:
return x - 2


def is_even_num(x: float) -> bool:
return x % 2 == 0


def square(x: float) -> float:
return x * x


def is_lt_400(x: float) -> float:
return x < 400 # noqa: PLR2004


def has_len(value: str) -> bool:
return len(value) > 0

Expand Down
174 changes: 174 additions & 0 deletions tests/test_benchmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from __future__ import annotations

from danom import Err, Ok, Result, Stream, all_of, any_of, compose, identity, invert, new_type
from tests.conftest import (
add_one,
double,
has_len,
is_even,
is_even_num,
is_gt_ten,
is_lt_400,
is_positive,
lt_100,
min_two,
square,
triple,
)


def test_stream_map(benchmark) -> None:
@benchmark
def _() -> None:
Stream.from_iterable(range(10_000)).map(add_one).collect()


def test_stream_filter(benchmark) -> None:
@benchmark
def _() -> None:
Stream.from_iterable(range(10_000)).filter(is_even).collect()


def test_stream_map_filter_chain(benchmark) -> None:
@benchmark
def _() -> None:
(
Stream.from_iterable(range(10_000))
.map(triple)
.filter(is_gt_ten) # ty: ignore[invalid-argument-type]
.map(min_two)
.filter(is_even_num)
.map(square)
.filter(is_lt_400) # ty: ignore[invalid-argument-type]
.collect()
)


def test_stream_fold(benchmark) -> None:
@benchmark
def _() -> None:
Stream.from_iterable(range(1_000)).fold(0, lambda a, b: a + b)


def test_stream_partition(benchmark) -> None:
@benchmark
def _() -> None:
Stream.from_iterable(range(1_000)).partition(is_even)


def test_ok_creation(benchmark) -> None:
@benchmark
def _() -> None:
for i in range(1_000):
Ok(i)


def test_err_creation(benchmark) -> None:
@benchmark
def _() -> None:
for i in range(1_000):
Err(error=i)


def test_ok_map_chain(benchmark) -> None:
@benchmark
def _() -> None:
Ok(1).map(add_one).map(double).map(add_one)


def test_ok_and_then_chain(benchmark) -> None:
def safe_add_one(x: float) -> Result:
return Ok(x + 1)

@benchmark
def _() -> None:
Ok(1).and_then(safe_add_one).and_then(safe_add_one).and_then(safe_add_one)


def test_err_map_short_circuit(benchmark) -> None:
@benchmark
def _() -> None:
Err(error=TypeError()).map(add_one).map(double).map(add_one)


def test_result_unwrap(benchmark) -> None:
ok = Ok(42)

@benchmark
def _() -> None:
ok.unwrap()


def test_stream_of_results(benchmark) -> None:
@benchmark
def _() -> None:
(
Stream.from_iterable([Ok(i) for i in range(100)])
.filter(Result.result_is_ok)
.map(Result.result_unwrap)
.collect()
)


def test_compose(benchmark) -> None:
fn = compose(add_one, double, add_one)

@benchmark
def _() -> None:
fn(5)


def test_all_of(benchmark) -> None:
fn = all_of(is_even, is_positive, lt_100)

@benchmark
def _() -> None:
fn(42)


def test_any_of(benchmark) -> None:
fn = any_of(is_even, is_positive, lt_100)

@benchmark
def _() -> None:
fn(42)


def test_identity(benchmark) -> None:
@benchmark
def _() -> None:
identity(42)


def test_invert(benchmark) -> None:
fn = invert(is_even)

@benchmark
def _() -> None:
fn(3)


PositiveFloat = new_type("PositiveFloat", float, validators=[is_positive])


def test_new_type_creation(benchmark) -> None:
@benchmark
def _() -> None:
PositiveFloat(42.0)


def test_new_type_map(benchmark) -> None:
val = PositiveFloat(10.0)

@benchmark
def _() -> None:
val.map(double)


StrType = new_type("StrType", str, validators=[has_len], converters=[str])


def test_new_type_with_converter(benchmark) -> None:
@benchmark
def _() -> None:
StrType(12345)
4 changes: 2 additions & 2 deletions tests/test_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_traceback():
"Traceback (most recent call last):",
' File "./src/danom/_safe.py", line 34, in __call__',
" return Ok(self.func(*args, **kwargs))",
' File "./tests/conftest.py", line 85, in div_zero',
' File "./tests/conftest.py", line 117, in div_zero',
" return x / 0",
"ZeroDivisionError: division by zero",
]
Expand All @@ -79,4 +79,4 @@ def test_traceback():

missing_lines = [line for line in expected_lines if line not in tb_lines]

assert missing_lines == []
assert missing_lines == [], f"lines don't match {tb_lines = }"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading