From 242ab632a2d6651876c8e19bcfabca7baff29290 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:21:51 +0000 Subject: [PATCH 1/3] Add CodSpeed benchmark suite and CI workflow --- .github/workflows/codspeed.yml | 34 ++++++ README.md | 2 +- tests/test_benchmarks.py | 204 +++++++++++++++++++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) 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..fd4a1c4 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,34 @@ +name: CodSpeed + +on: + push: + branches: + - "main" + pull_request: + 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..7527134 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 diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 0000000..e7d4c92 --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from danom import Err, Ok, Result, Stream, all_of, any_of, compose, identity, invert, new_type + +# --------------------------------------------------------------------------- +# Helper functions used by benchmarks +# --------------------------------------------------------------------------- + + +def add_one(x: float) -> float: + return x + 1 + + +def double(x: float) -> float: + return x * 2 + + +def is_even(x: float) -> bool: + return x % 2 == 0 + + +def is_positive(x: float) -> bool: + return x > 0 + + +def lt_100(x: float) -> bool: + return x < 100 + + +def has_len(value: str) -> bool: + return len(value) > 0 + + +# --------------------------------------------------------------------------- +# Type definitions used by benchmarks +# --------------------------------------------------------------------------- + +positive_float_type = new_type("PositiveFloat", float, validators=[is_positive]) +str_type = new_type("StrType", str, validators=[has_len], converters=[str]) + + +# --------------------------------------------------------------------------- +# Stream benchmarks +# --------------------------------------------------------------------------- + + +class TestStreamBenchmarks: + def test_stream_map_small(self, benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(100)).map(add_one).collect() + + def test_stream_map_medium(self, benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(10_000)).map(add_one).collect() + + def test_stream_filter_small(self, benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(100)).filter(is_even).collect() + + def test_stream_filter_medium(self, benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(10_000)).filter(is_even).collect() + + def test_stream_map_filter_chain(self, benchmark) -> None: + @benchmark + def _() -> None: + ( + Stream.from_iterable(range(1_000)) + .map(add_one, double) + .filter(is_even, lt_100) + .collect() + ) + + def test_stream_fold(self, benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(1_000)).fold(0, lambda a, b: a + b) + + def test_stream_partition(self, benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(1_000)).partition(is_even) + + +# --------------------------------------------------------------------------- +# Result benchmarks +# --------------------------------------------------------------------------- + + +class TestResultBenchmarks: + def test_ok_creation(self, benchmark) -> None: + @benchmark + def _() -> None: + for i in range(1_000): + Ok(i) + + def test_err_creation(self, benchmark) -> None: + @benchmark + def _() -> None: + for i in range(1_000): + Err(error=i) + + def test_ok_map_chain(self, benchmark) -> None: + @benchmark + def _() -> None: + Ok(1).map(add_one).map(double).map(add_one) + + def test_ok_and_then_chain(self, 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(self, benchmark) -> None: + @benchmark + def _() -> None: + Err(error=TypeError()).map(add_one).map(double).map(add_one) + + def test_result_unwrap(self, benchmark) -> None: + ok = Ok(42) + + @benchmark + def _() -> None: + ok.unwrap() + + def test_stream_of_results(self, 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() + ) + + +# --------------------------------------------------------------------------- +# Utility function benchmarks +# --------------------------------------------------------------------------- + + +class TestUtilsBenchmarks: + def test_compose(self, benchmark) -> None: + fn = compose(add_one, double, add_one) + + @benchmark + def _() -> None: + fn(5) + + def test_all_of(self, benchmark) -> None: + fn = all_of(is_even, is_positive, lt_100) + + @benchmark + def _() -> None: + fn(42) + + def test_any_of(self, benchmark) -> None: + fn = any_of(is_even, is_positive, lt_100) + + @benchmark + def _() -> None: + fn(42) + + def test_identity(self, benchmark) -> None: + @benchmark + def _() -> None: + identity(42) + + def test_invert(self, benchmark) -> None: + fn = invert(is_even) + + @benchmark + def _() -> None: + fn(3) + + +# --------------------------------------------------------------------------- +# NewType benchmarks +# --------------------------------------------------------------------------- + + +class TestNewTypeBenchmarks: + def test_new_type_creation(self, benchmark) -> None: + @benchmark + def _() -> None: + positive_float_type(42.0) + + def test_new_type_map(self, benchmark) -> None: + val = positive_float_type(10.0) + + @benchmark + def _() -> None: + val.map(double) + + def test_new_type_with_converter(self, benchmark) -> None: + @benchmark + def _() -> None: + str_type(12345) From e33302a15ff8625cf38cfe358b6db906584864ad Mon Sep 17 00:00:00 2001 From: ed cuss Date: Thu, 26 Mar 2026 20:43:01 +0000 Subject: [PATCH 2/3] fix: make tests more me like --- .github/workflows/codspeed.yml | 3 +- README.md | 1 + tests/conftest.py | 32 ++++ tests/test_benchmarks.py | 298 ++++++++++++++++----------------- tests/test_safe.py | 4 +- 5 files changed, 177 insertions(+), 161 deletions(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index fd4a1c4..416ac80 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -2,9 +2,10 @@ name: CodSpeed on: push: + pull_request: branches: - "main" - pull_request: + - "dev" workflow_dispatch: permissions: diff --git a/README.md b/README.md index 7527134..bda99cc 100644 --- a/README.md +++ b/README.md @@ -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 index e7d4c92..293c8f0 100644 --- a/tests/test_benchmarks.py +++ b/tests/test_benchmarks.py @@ -1,204 +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, +) -# --------------------------------------------------------------------------- -# Helper functions used by benchmarks -# --------------------------------------------------------------------------- +def test_stream_map_small(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(100)).map(add_one).collect() -def add_one(x: float) -> float: - return x + 1 +def test_stream_map_medium(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(10_000)).map(add_one).collect() -def double(x: float) -> float: - return x * 2 +def test_stream_filter_small(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(100)).filter(is_even).collect() -def is_even(x: float) -> bool: - return x % 2 == 0 +def test_stream_filter_medium(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(10_000)).filter(is_even).collect() -def is_positive(x: float) -> bool: - return x > 0 +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 lt_100(x: float) -> bool: - return x < 100 +def test_stream_fold(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(1_000)).fold(0, lambda a, b: a + b) -def has_len(value: str) -> bool: - return len(value) > 0 +def test_stream_partition(benchmark) -> None: + @benchmark + def _() -> None: + Stream.from_iterable(range(1_000)).partition(is_even) -# --------------------------------------------------------------------------- -# Type definitions used by benchmarks -# --------------------------------------------------------------------------- -positive_float_type = new_type("PositiveFloat", float, validators=[is_positive]) -str_type = new_type("StrType", str, validators=[has_len], converters=[str]) +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) -# --------------------------------------------------------------------------- -# Stream benchmarks -# --------------------------------------------------------------------------- - - -class TestStreamBenchmarks: - def test_stream_map_small(self, benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(100)).map(add_one).collect() - - def test_stream_map_medium(self, benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(10_000)).map(add_one).collect() - - def test_stream_filter_small(self, benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(100)).filter(is_even).collect() - def test_stream_filter_medium(self, benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(10_000)).filter(is_even).collect() - - def test_stream_map_filter_chain(self, benchmark) -> None: - @benchmark - def _() -> None: - ( - Stream.from_iterable(range(1_000)) - .map(add_one, double) - .filter(is_even, lt_100) - .collect() - ) +def test_err_map_short_circuit(benchmark) -> None: + @benchmark + def _() -> None: + Err(error=TypeError()).map(add_one).map(double).map(add_one) - def test_stream_fold(self, benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(1_000)).fold(0, lambda a, b: a + b) - def test_stream_partition(self, benchmark) -> None: - @benchmark - def _() -> None: - Stream.from_iterable(range(1_000)).partition(is_even) +def test_result_unwrap(benchmark) -> None: + ok = Ok(42) + @benchmark + def _() -> None: + ok.unwrap() -# --------------------------------------------------------------------------- -# Result benchmarks -# --------------------------------------------------------------------------- +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() + ) -class TestResultBenchmarks: - def test_ok_creation(self, benchmark) -> None: - @benchmark - def _() -> None: - for i in range(1_000): - Ok(i) - def test_err_creation(self, benchmark) -> None: - @benchmark - def _() -> None: - for i in range(1_000): - Err(error=i) +def test_compose(benchmark) -> None: + fn = compose(add_one, double, add_one) - def test_ok_map_chain(self, benchmark) -> None: - @benchmark - def _() -> None: - Ok(1).map(add_one).map(double).map(add_one) + @benchmark + def _() -> None: + fn(5) - def test_ok_and_then_chain(self, 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_all_of(benchmark) -> None: + fn = all_of(is_even, is_positive, lt_100) - def test_err_map_short_circuit(self, benchmark) -> None: - @benchmark - def _() -> None: - Err(error=TypeError()).map(add_one).map(double).map(add_one) + @benchmark + def _() -> None: + fn(42) - def test_result_unwrap(self, benchmark) -> None: - ok = Ok(42) - @benchmark - def _() -> None: - ok.unwrap() +def test_any_of(benchmark) -> None: + fn = any_of(is_even, is_positive, lt_100) - def test_stream_of_results(self, 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() - ) - - -# --------------------------------------------------------------------------- -# Utility function benchmarks -# --------------------------------------------------------------------------- - - -class TestUtilsBenchmarks: - def test_compose(self, benchmark) -> None: - fn = compose(add_one, double, add_one) - - @benchmark - def _() -> None: - fn(5) - - def test_all_of(self, benchmark) -> None: - fn = all_of(is_even, is_positive, lt_100) - - @benchmark - def _() -> None: - fn(42) + @benchmark + def _() -> None: + fn(42) - def test_any_of(self, benchmark) -> None: - fn = any_of(is_even, is_positive, lt_100) - - @benchmark - def _() -> None: - fn(42) - - def test_identity(self, benchmark) -> None: - @benchmark - def _() -> None: - identity(42) - def test_invert(self, benchmark) -> None: - fn = invert(is_even) +def test_identity(benchmark) -> None: + @benchmark + def _() -> None: + identity(42) - @benchmark - def _() -> None: - fn(3) +def test_invert(benchmark) -> None: + fn = invert(is_even) -# --------------------------------------------------------------------------- -# NewType benchmarks -# --------------------------------------------------------------------------- + @benchmark + def _() -> None: + fn(3) -class TestNewTypeBenchmarks: - def test_new_type_creation(self, benchmark) -> None: - @benchmark - def _() -> None: - positive_float_type(42.0) +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(self, benchmark) -> None: - val = positive_float_type(10.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]) - @benchmark - def _() -> None: - val.map(double) - def test_new_type_with_converter(self, benchmark) -> None: - @benchmark - def _() -> None: - str_type(12345) +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 c098b8e517180060932f7e9a60ded09a62a3f917 Mon Sep 17 00:00:00 2001 From: ed cuss Date: Thu, 26 Mar 2026 20:44:42 +0000 Subject: [PATCH 3/3] fix: don't run on all commits --- .github/workflows/codspeed.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 416ac80..85e5958 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -1,7 +1,6 @@ name: CodSpeed on: - push: pull_request: branches: - "main"