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/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/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] 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..87ee454 --- /dev/null +++ b/tests/test_benchmarks.py @@ -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) 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 = }" 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" },