From 952a073f2ae36fed40c33a4bc8997d5d2fd07961 Mon Sep 17 00:00:00 2001 From: "codspeed-hq[bot]" <117304815+codspeed-hq[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:48:00 +0000 Subject: [PATCH 1/4] Add CodSpeed performance benchmarks (#24) * Add CodSpeed benchmark suite and CI workflow * fix: make tests more me like * fix: don't run on all commits --------- Co-authored-by: codspeed-hq[bot] <117304815+codspeed-hq[bot]@users.noreply.github.com> Co-authored-by: ed cuss --- .github/workflows/codspeed.yml | 34 ++++++ README.md | 3 +- tests/conftest.py | 32 ++++++ tests/test_benchmarks.py | 186 +++++++++++++++++++++++++++++++++ tests/test_safe.py | 4 +- 5 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/codspeed.yml create mode 100644 tests/test_benchmarks.py diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..85e5958 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -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 diff --git a/README.md b/README.md index 7e3a2d3..bda99cc 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index bf93208..9ba58e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 0000000..293c8f0 --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,186 @@ +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_small(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(100)).map(add_one).collect() + + +def test_stream_map_medium(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(10_000)).map(add_one).collect() + + +def test_stream_filter_small(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(100)).filter(is_even).collect() + + +def test_stream_filter_medium(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(100_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) + + +positive_float_type = new_type("PositiveFloat", float, validators=[is_positive]) + + +def test_new_type_creation(benchmark) -> None: + @benchmark + def _() -> None: + positive_float_type(42.0) + + +def test_new_type_map(benchmark) -> None: + val = positive_float_type(10.0) + + @benchmark + def _() -> None: + val.map(double) + + +str_type = new_type("StrType", str, validators=[has_len], converters=[str]) + + +def test_new_type_with_converter(benchmark) -> None: + @benchmark + def _() -> None: + str_type(12345) diff --git a/tests/test_safe.py b/tests/test_safe.py index e9ea1a2..562802d 100644 --- a/tests/test_safe.py +++ b/tests/test_safe.py @@ -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", ] @@ -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 = }" From c287773a9763cb24c35638761a037aa30989232a Mon Sep 17 00:00:00 2001 From: Ed Cuss <100875124+second-ed@users.noreply.github.com> Date: Thu, 26 Mar 2026 20:58:49 +0000 Subject: [PATCH 2/4] Fix/minor changes (#25) * Add CodSpeed benchmark suite and CI workflow * fix: make tests more me like * fix: don't run on all commits * test: remove small versions of tests --------- Co-authored-by: codspeed-hq[bot] <117304815+codspeed-hq[bot]@users.noreply.github.com> --- tests/test_benchmarks.py | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py index 293c8f0..87ee454 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -17,25 +17,13 @@ ) -def test_stream_map_small(benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(100)).map(add_one).collect() - - -def test_stream_map_medium(benchmark) -> None: +def test_stream_map(benchmark) -> None: @benchmark def _() -> None: Stream.from_iterable(range(10_000)).map(add_one).collect() -def test_stream_filter_small(benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(100)).filter(is_even).collect() - - -def test_stream_filter_medium(benchmark) -> None: +def test_stream_filter(benchmark) -> None: @benchmark def _() -> None: Stream.from_iterable(range(10_000)).filter(is_even).collect() @@ -45,7 +33,7 @@ def test_stream_map_filter_chain(benchmark) -> None: @benchmark def _() -> None: ( - Stream.from_iterable(range(100_000)) + Stream.from_iterable(range(10_000)) .map(triple) .filter(is_gt_ten) # ty: ignore[invalid-argument-type] .map(min_two) @@ -160,27 +148,27 @@ def _() -> None: fn(3) -positive_float_type = new_type("PositiveFloat", float, validators=[is_positive]) +PositiveFloat = new_type("PositiveFloat", float, validators=[is_positive]) def test_new_type_creation(benchmark) -> None: @benchmark def _() -> None: - positive_float_type(42.0) + PositiveFloat(42.0) def test_new_type_map(benchmark) -> None: - val = positive_float_type(10.0) + val = PositiveFloat(10.0) @benchmark def _() -> None: val.map(double) -str_type = new_type("StrType", str, validators=[has_len], converters=[str]) +StrType = new_type("StrType", str, validators=[has_len], converters=[str]) def test_new_type_with_converter(benchmark) -> None: @benchmark def _() -> None: - str_type(12345) + StrType(12345) From 882f1b0cea5ec445c7faf84581b7fb6a110524db Mon Sep 17 00:00:00 2001 From: Ed Cuss <100875124+second-ed@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:51:44 +0100 Subject: [PATCH 3/4] perf: run fn once in loop (#26) --- src/danom/_stream.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/danom/_stream.py b/src/danom/_stream.py index e85bb21..f301dc4 100644 --- a/src/danom/_stream.py +++ b/src/danom/_stream.py @@ -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 @@ -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 @@ -442,7 +447,7 @@ async def async_collect(self) -> Awaitable[tuple[U, ...]]: class _Nothing(Enum): - NOTHING = auto() + NOTHING = 0 PlannedOps = tuple[str, StreamFn] From 8858381baee44faa9162e61dfd5c54bb4347900f Mon Sep 17 00:00:00 2001 From: ed cuss Date: Sun, 29 Mar 2026 10:53:21 +0100 Subject: [PATCH 4/4] bump: => 0.11.3 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fae79a9..73aa687 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/uv.lock b/uv.lock index 30f0b57..21352d7 100644 --- a/uv.lock +++ b/uv.lock @@ -317,7 +317,7 @@ wheels = [ [[package]] name = "danom" -version = "0.11.2" +version = "0.11.3" source = { editable = "." } dependencies = [ { name = "attrs" },