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 = }"