diff --git a/benchmarks.py b/benchmarks.py new file mode 100644 index 0000000..77f6f2e --- /dev/null +++ b/benchmarks.py @@ -0,0 +1,55 @@ +# ruff: noqa: D100, D103 + +from functools import reduce +from typing import Any, Callable + +from pytest_benchmark.fixture import BenchmarkFixture +from stateless import ( + Ability, + Depend, + Handler, + Need, + Success, + handle, + need, + run, + supply, +) +from stateless.errors import UnhandledAbilityError +from typing_extensions import Never + + +def never_handler(_: Ability[Any]) -> Never: + raise UnhandledAbilityError() + + +dummy_handler: Handler[Never] = handle(never_handler) # type: ignore + + +def create_effect_chain(chain_length: int, n_yields: int) -> Callable[[], Success[str]]: + def base() -> Depend[Need[str], str]: + for i in range(n_yields): + yield from need(str) + return "done" + + def wrap_test_handler( + f: Callable[[], Depend[Need[str], str]], + ) -> Callable[[], Depend[Need[str], str]]: + return dummy_handler(f) + + g = reduce(lambda acc, _: wrap_test_handler(acc), range(chain_length - 1), base) + + return supply("")(g) + + +def test_effect_chain(benchmark: BenchmarkFixture) -> None: + """Benchmark a long chain of functions using functools.reduce.""" + + effect = create_effect_chain(500, 0)() + benchmark(run, effect) + + +def test_handler_chain(benchmark: BenchmarkFixture) -> None: + effect = create_effect_chain(chain_length=500, n_yields=500)() + + benchmark(run, effect) diff --git a/poetry.lock b/poetry.lock index be0ef17..69fd69c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -485,19 +485,19 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co [[package]] name = "pluggy" -version = "1.3.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -561,6 +561,18 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + [[package]] name = "pygments" version = "2.16.1" @@ -599,26 +611,48 @@ nodejs = ["nodejs-wheel-binaries"] [[package]] name = "pytest" -version = "7.4.3" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-benchmark" +version = "5.2.1" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_benchmark-5.2.1-py3-none-any.whl", hash = "sha256:a6e18fe0df2155e9d993db6ba03bdf85324794035ad986553787024ca59e8db9"}, + {file = "pytest_benchmark-5.2.1.tar.gz", hash = "sha256:56dc1455bda7ccb540aa671c496dafc8187d2769f278e5f137689476805b6f9d"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=8.1" [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs", "setuptools"] [[package]] name = "pyyaml" @@ -848,4 +882,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "a7d793c47c20ac4d3e5052430b0d698262a87b591a17ec8b54652510de6b4982" +content-hash = "2ad5075792eefb92e61b7d5eb29744742ae1be69311d5efc746486e385ed29fb" diff --git a/pyproject.toml b/pyproject.toml index c8bcb1d..28abf12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,12 +15,13 @@ cloudpickle = "^3.0.0" mypy = "^1.6.1" ipdb = "^0.13.13" ipython = "^8.17.2" -pytest = "^7.4.3" +pytest = "^8" pyright = "^1.1.336" pre-commit = "^3.5.0" ruff = "^0.14.2" coverage = "^7.3.2" toml = "^0.10.2" +pytest-benchmark = "^5.2.1" [tool.mypy]