diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..ef63aed --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,40 @@ +name: Benchmark + +on: [push, pull_request, workflow_dispatch] + +permissions: {} + +env: + FORCE_COLOR: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 + +jobs: + benchmarks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Python 3.14 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + allow-prereleases: true + + - name: Install gettext + run: | + sudo apt install gettext + + - name: Generate translation binaries + run: | + scripts/generate-translation-binaries.sh + + - name: Install dependencies + run: pip install -e ".[tests]" + + - name: Run benchmarks + uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1 + with: + mode: simulation + run: pytest tests/test_benchmarks.py --codspeed diff --git a/pyproject.toml b/pyproject.toml index 5cbff14..07fe9a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ dynamic = [ "version" ] optional-dependencies.tests = [ "freezegun", "pytest>=9", + "pytest-benchmark", + "pytest-codspeed", "pytest-cov", ] urls."Issue tracker" = "https://github.com/python-humanize/humanize/issues" diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 0000000..c200dd0 --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,82 @@ +"""Benchmarks run via pytest-codspeed.""" + +from __future__ import annotations + +import datetime as dt + +import pytest + +import humanize + +TYPE_CHECKING = False +if TYPE_CHECKING: + from pytest_codspeed import BenchmarkFixture + + +@pytest.fixture(scope="session", autouse=True) +def _warmup_i18n() -> None: + # Touch i18n-backed functions once so lazy gettext catalog loading + # is not attributed to the first benchmarked call. + humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=1)) + humanize.naturalday(dt.date.today()) + + +def test_apnumber(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.apnumber, 7) + + +def test_clamp(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.clamp, 0.5, "{:.0%}", 0.1, 0.9) + + +def test_fractional(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.fractional, 1.5) + + +def test_intcomma(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.intcomma, 1_234_567_890) + + +def test_intword(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.intword, 1_234_567_890) + + +def test_metric(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.metric, 1500, "W") + + +def test_natural_list(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.natural_list, ["one", "two", "three", "four"]) + + +def test_naturaldate(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.naturaldate, dt.date.today() - dt.timedelta(days=30)) + + +def test_naturalday(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.naturalday, dt.date.today() - dt.timedelta(days=1)) + + +def test_naturaldelta(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.naturaldelta, dt.timedelta(hours=3, minutes=27)) + + +def test_naturalsize(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.naturalsize, 1_234_567_890) + + +def test_naturaltime(benchmark: BenchmarkFixture) -> None: + when = dt.datetime.now() - dt.timedelta(hours=3, minutes=27) + benchmark(humanize.naturaltime, when) + + +def test_ordinal(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.ordinal, 123) + + +def test_precisedelta(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.precisedelta, dt.timedelta(days=2, hours=3, seconds=4)) + + +def test_scientific(benchmark: BenchmarkFixture) -> None: + benchmark(humanize.scientific, -1.234e-50)