From e56f2e853613f874dbc755907d8ac88f4f89eb36 Mon Sep 17 00:00:00 2001 From: CodSpeed Bot Date: Sun, 21 Jun 2026 21:54:20 +0000 Subject: [PATCH 1/2] Add CodSpeed performance benchmarks and CI workflow --- .github/workflows/codspeed.yml | 62 ++++++++++++++++ README.md | 1 + benchmarks/test_benchmarks.py | 132 +++++++++++++++++++++++++++++++++ poetry.lock | 71 +++++++++++++++++- pyproject.toml | 4 + 5 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/codspeed.yml create mode 100644 benchmarks/test_benchmarks.py diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 00000000..503707e6 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,62 @@ +name: CodSpeed + +on: + push: + branches: + - master + pull_request: + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write # for OpenID Connect authentication with CodSpeed + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + benchmarks: + strategy: + fail-fast: false + matrix: + include: + - mode: instrumentation + runner: ubuntu-latest + - mode: walltime + runner: codspeed-macro + - mode: memory + runner: ubuntu-latest + + name: Run benchmarks (${{ matrix.mode }}) + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v6.0.3 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install poetry + run: | + curl -sSL "https://install.python-poetry.org" | python + + # Adding `poetry` to `$PATH`: + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + poetry config virtualenvs.in-project true + poetry install --all-extras + + - name: Run benchmarks + uses: CodSpeedHQ/action@v4.17.6 + with: + mode: ${{ matrix.mode }} + run: poetry run pytest benchmarks/ --codspeed -p no:cov -o addopts="" diff --git a/README.md b/README.md index d61605ed..886fec5d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![test](https://github.com/dry-python/returns/actions/workflows/test.yml/badge.svg?branch=master&event=push)](https://github.com/dry-python/returns/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/dry-python/returns/branch/master/graph/badge.svg)](https://codecov.io/gh/dry-python/returns) +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://app.codspeed.io/dry-python/returns?utm_source=badge) [![Documentation Status](https://readthedocs.org/projects/returns/badge/?version=latest)](https://returns.readthedocs.io/en/latest/?badge=latest) [![Python Version](https://img.shields.io/pypi/pyversions/returns.svg)](https://pypi.org/project/returns/) [![conda](https://img.shields.io/conda/v/conda-forge/returns?label=conda)](https://anaconda.org/conda-forge/returns) diff --git a/benchmarks/test_benchmarks.py b/benchmarks/test_benchmarks.py new file mode 100644 index 00000000..648c4be9 --- /dev/null +++ b/benchmarks/test_benchmarks.py @@ -0,0 +1,132 @@ +"""Performance benchmarks for the core ``returns`` containers. + +These benchmarks exercise the hot paths of the most commonly used +containers (``Result``, ``Maybe``, ``IO``) together with the pipeline +and iterable helpers. They are measured by CodSpeed in CI. +""" + +from returns.io import IO +from returns.iterables import Fold +from returns.maybe import Maybe, Nothing, Some +from returns.pipeline import flow +from returns.pointfree import bind, map_ +from returns.result import Failure, Result, Success, safe + + +def _increment(value: int) -> int: + return value + 1 + + +def _as_success(value: int) -> Result[int, str]: + return Success(value + 1) + + +def _as_some(value: int) -> Maybe[int]: + return Some(value + 1) + + +def test_result_map_chain(benchmark) -> None: + """A long chain of ``.map`` calls over a ``Result``.""" + + def run() -> Result[int, str]: + container: Result[int, str] = Success(0) + for _ in range(100): + container = container.map(_increment) + return container + + assert benchmark(run) == Success(100) + + +def test_result_bind_chain(benchmark) -> None: + """A long chain of ``.bind`` calls over a ``Result``.""" + + def run() -> Result[int, str]: + container: Result[int, str] = Success(0) + for _ in range(100): + container = container.bind(_as_success) + return container + + assert benchmark(run) == Success(100) + + +def test_result_failure_lash(benchmark) -> None: + """Recover from a failure using ``.lash`` and ``.value_or``.""" + + def run() -> int: + container: Result[int, str] = Failure('boom') + return container.lash(lambda _: Success(42)).value_or(0) + + assert benchmark(run) == 42 + + +def test_safe_decorator(benchmark) -> None: + """The ``@safe`` decorator wrapping a raising function.""" + + @safe + def _divide(numerator: int, denominator: int) -> float: + return numerator / denominator + + def run() -> Result[float, Exception]: + return _divide(10, 0) + + result = benchmark(run) + assert isinstance(result, Failure) + + +def test_maybe_map_chain(benchmark) -> None: + """A long chain of ``.map`` calls over a ``Maybe``.""" + + def run() -> Maybe[int]: + container: Maybe[int] = Some(0) + for _ in range(100): + container = container.map(_increment) + return container + + assert benchmark(run) == Some(100) + + +def test_maybe_bind_nothing(benchmark) -> None: + """Short-circuiting a ``Maybe`` chain through ``Nothing``.""" + + def run() -> int: + container: Maybe[int] = Some(1) + container = container.bind(lambda _: Nothing) + return container.bind(_as_some).value_or(-1) + + assert benchmark(run) == -1 + + +def test_io_map_chain(benchmark) -> None: + """A long chain of ``.map`` calls over an ``IO`` container.""" + + def run() -> IO[int]: + container = IO(0) + for _ in range(100): + container = container.map(_increment) + return container + + assert benchmark(run) == IO(100) + + +def test_flow_pipeline(benchmark) -> None: + """Compose containers through ``flow`` with point-free helpers.""" + + def run() -> Result[int, str]: + return flow( + Success(1), + map_(_increment), + bind(_as_success), + map_(_increment), + ) + + assert benchmark(run) == Success(4) + + +def test_fold_collect_results(benchmark) -> None: + """Fold an iterable of ``Result`` values into a single container.""" + items = [Success(index) for index in range(100)] + + def run() -> Result[tuple[int, ...], str]: + return Fold.collect(items, Success(())) + + assert benchmark(run) == Success(tuple(range(100))) diff --git a/poetry.lock b/poetry.lock index e4618759..74bdcf5f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -942,7 +942,7 @@ version = "4.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" -groups = ["docs"] +groups = ["dev", "docs"] files = [ {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, @@ -1097,7 +1097,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" -groups = ["docs"] +groups = ["dev", "docs"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -1354,6 +1354,52 @@ pygments = ">=2.7.2" [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-codspeed" +version = "5.0.3" +description = "Pytest plugin to create CodSpeed benchmarks" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_codspeed-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63"}, + {file = "pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510"}, + {file = "pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693"}, + {file = "pytest_codspeed-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093"}, + {file = "pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c"}, + {file = "pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9"}, + {file = "pytest_codspeed-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee"}, + {file = "pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7"}, + {file = "pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf"}, + {file = "pytest_codspeed-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4"}, + {file = "pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901"}, + {file = "pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad"}, + {file = "pytest_codspeed-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bce0a6ea93a5b19658f713312bb67554c19283ab15b454a1e3e55a13e78130f8"}, + {file = "pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a2097247985f5d915a94b80c5552d10979ca858c859fc3edef1bf2baa5c9b7a"}, + {file = "pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e192905a2230f9956e6160732f76577836953a4a1fb2b1e7be74e51ac7b2a0"}, + {file = "pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378"}, + {file = "pytest_codspeed-5.0.3.tar.gz", hash = "sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f"}, +] + +[package.dependencies] +pytest = ">=3.8" +rich = ">=13.8.1" + +[package.extras] +compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] + [[package]] name = "pytest-cov" version = "7.1.0" @@ -1689,6 +1735,25 @@ urllib3 = ">=1.26,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "roman-numerals" version = "4.1.0" @@ -2314,4 +2379,4 @@ compatible-mypy = ["mypy"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "6cc449cab1c95ca83835d85e8545dcc82d76f27e732101e2d7f5ac72a9800cc7" +content-hash = "fe979c6275ae5882416d015099c547940a4f271eebe28ed7de6017cc6716ee7b" diff --git a/pyproject.toml b/pyproject.toml index 25b6f548..f741c66c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ pytest-mypy-plugins = ">=3.1,<5.0" pytest-subtests = ">=0.14,<0.16" pytest-shard = "^0.1" covdefaults = "^2.3" +pytest-codspeed = "^5.0.3" [tool.poetry.group.docs] optional = true @@ -169,6 +170,9 @@ pydocstyle.convention = "google" [tool.ruff.lint.per-file-ignores] "*.pyi" = ["D103"] +"benchmarks/*.py" = [ + "S101", # asserts +] "returns/context/__init__.py" = ["F401", "PLC0414"] "returns/contrib/mypy/*.py" = ["S101"] "returns/contrib/mypy/_typeops/visitor.py" = ["S101"] From 64a2819ba58a322840e7932cf17ae45af885391b Mon Sep 17 00:00:00 2001 From: CodSpeed Bot Date: Mon, 22 Jun 2026 07:57:41 +0000 Subject: [PATCH 2/2] Add do-notation benchmarks and loosen pytest-codspeed constraint --- benchmarks/test_benchmarks.py | 26 ++++++++++++++++++++++++++ poetry.lock | 2 +- pyproject.toml | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/benchmarks/test_benchmarks.py b/benchmarks/test_benchmarks.py index 648c4be9..1d82a901 100644 --- a/benchmarks/test_benchmarks.py +++ b/benchmarks/test_benchmarks.py @@ -49,6 +49,32 @@ def run() -> Result[int, str]: assert benchmark(run) == Success(100) +def test_result_do_notation(benchmark) -> None: + """Compose ``Result`` values through ``.do`` notation.""" + + def run() -> Result[int, str]: + return Result.do( + first + second + for first in Success(1) + for second in Success(2) + ) + + assert benchmark(run) == Success(3) + + +def test_maybe_do_notation(benchmark) -> None: + """Compose ``Maybe`` values through ``.do`` notation.""" + + def run() -> Maybe[int]: + return Maybe.do( + first + second + for first in Some(1) + for second in Some(2) + ) + + assert benchmark(run) == Some(3) + + def test_result_failure_lash(benchmark) -> None: """Recover from a failure using ``.lash`` and ``.value_or``.""" diff --git a/poetry.lock b/poetry.lock index 74bdcf5f..34fda815 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2379,4 +2379,4 @@ compatible-mypy = ["mypy"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "fe979c6275ae5882416d015099c547940a4f271eebe28ed7de6017cc6716ee7b" +content-hash = "42a0aac18db574b3ff3e739701fa57320622c5c8ace28597f130b2ce44284b40" diff --git a/pyproject.toml b/pyproject.toml index f741c66c..9d1cf0d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ pytest-mypy-plugins = ">=3.1,<5.0" pytest-subtests = ">=0.14,<0.16" pytest-shard = "^0.1" covdefaults = "^2.3" -pytest-codspeed = "^5.0.3" +pytest-codspeed = "^5.0" [tool.poetry.group.docs] optional = true