Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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: 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'
Comment thread
thepabloaguilar marked this conversation as resolved.

- 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=""
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
155 changes: 155 additions & 0 deletions benchmarks/test_benchmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""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:
Comment thread
thepabloaguilar marked this conversation as resolved.
"""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_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``."""
value = 42

def run() -> int:
container: Result[int, str] = Failure('boom')
return container.lash(lambda _: Success(value)).value_or(0)

assert benchmark(run) == value


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)))
71 changes: 68 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pytest-randomly = ">=3.12,<5.0"
pytest-mypy-plugins = ">=3.1,<5.0"
pytest-subtests = ">=0.14,<0.16"
pytest-shard = "^0.1"
pytest-codspeed = "^5.0"
covdefaults = "^2.3"

[tool.poetry.group.docs]
Expand Down Expand Up @@ -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"]
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ per-file-ignores =
tests/test_examples/test_maybe/test_maybe_pattern_matching.py: D101, D103, F811, WPS306, WPS421
tests/test_examples/test_io/test_ioresult_container/test_ioresult_pattern_matching.py: WPS110, WPS421, WPS432
tests/test_pattern_matching.py: S101, WPS110, WPS218, WPS432
# Benchmarks
benchmarks/*: WPS110, WPS204, WPS430
# Annotations:
*.pyi: D103, WPS112, WPS211, WPS428

Expand Down
Loading