diff --git a/.gitignore b/.gitignore index 3306f6b..6dc3315 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ coverage.xml .dmypy.json .hypothesis/ .idea/ +.codspeed +uv.lock diff --git a/pyproject.toml b/pyproject.toml index 105017d..c59fe25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ allow_untyped_defs = false # Allow test functions to be untyped module = "tests.*" allow_untyped_defs = true +disallow_untyped_calls = false [tool.pylint.MASTER] persistent = "no" diff --git a/tests/test_benchmark_http.py b/tests/test_benchmark_http.py new file mode 100644 index 0000000..f578832 --- /dev/null +++ b/tests/test_benchmark_http.py @@ -0,0 +1,60 @@ +from collections import OrderedDict + +import pytest + +from w3lib.http import headers_dict_to_raw, headers_raw_to_dict + +pytest.importorskip("pytest_codspeed", reason="Benchmark tests require pytest-codspeed") + +pytestmark = pytest.mark.benchmark + + +def _header_case_long_headers(): + dct = OrderedDict( + [ + (b"X-Custom-Header", [b"a" * 1_000]), + (b"X-Custom-Header-2", [b"b" * 1_000]), + ] + ) + raw = ( + b"X-Custom-Header: " + b"a" * 1_000 + b"\r\n" + b"X-Custom-Header-2: " + b"b" * 1_000 + ) # fmt: off + return "long_headers", dct, raw + + +def _header_case_many_unique_headers(): + dct = OrderedDict( + [(f"Header-{i}".encode(), [f"value-{i}".encode()]) for i in range(100)] + ) + raw = b"\r\n".join([f"Header-{i}: value-{i}".encode() for i in range(100)]) + return "many_unique_headers", dct, raw + + +def _header_case_many_repeated_headers(): + values = [f"id={i}".encode() for i in range(100)] + dct = OrderedDict([(b"Set-Cookie", values)]) + raw = b"\r\n".join([b"Set-Cookie: " + val for val in values]) + return "many_repeated_headers", dct, raw + + +header_cases = [ + _header_case_long_headers(), + _header_case_many_unique_headers(), + _header_case_many_repeated_headers(), +] + + +@pytest.mark.parametrize( + ("_id", "dct", "raw"), + header_cases, + ids=[case[0] for case in header_cases], +) +class TestBenchmarkHttp: + def test_bench_dict_to_raw(self, benchmark, _id, dct, raw): # noqa: PT019 + result = benchmark(lambda: headers_dict_to_raw(dct)) + assert result == raw + + def test_bench_raw_to_dict(self, benchmark, _id, dct, raw): # noqa: PT019 + result = benchmark(lambda: headers_raw_to_dict(raw)) + assert result == dct diff --git a/tests/test_http.py b/tests/test_http.py index e0ece39..a212c65 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -55,7 +55,10 @@ def test_headers_dict_to_raw(self): def test_headers_dict_to_raw_listtuple(self): dct: HeadersDictInput = OrderedDict( - [(b"Content-type", [b"text/html"]), (b"Accept", [b"gzip"])] + [ + (b"Content-type", [b"text/html"]), + (b"Accept", [b"gzip"]), + ] ) assert headers_dict_to_raw(dct) == b"Content-type: text/html\r\nAccept: gzip" diff --git a/tox.ini b/tox.ini index 9a2de58..4e092dd 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,20 @@ # and then run "tox" from this directory. [tox] -envlist = py39, py310, py311, py312, py313, py314, pypy3.10, pypy3.11, docs, pylint, typing, pre-commit, twinecheck +envlist = + py39 + py310 + py311 + py312 + py313 + py314 + pypy3.10 + pypy3.11 + docs + pylint + typing + pre-commit + twinecheck [testenv] deps = @@ -51,3 +64,23 @@ deps = commands = python -m build --sdist twine check dist/* + +[testenv:benchmark-{py39,py310,py311,py312,py313,py314,pypy310,pypy311}] +basepython = + py39: python3.9 + py310: python3.10 + py311: python3.11 + py312: python3.12 + py313: python3.13 + py314: python3.14 + pypy310: pypy3.10 + pypy311: pypy3.11 +deps = + pytest + pytest-codspeed +commands = + python -m pytest \ + --codspeed \ + --codspeed-warmup-time=1 \ + --codspeed-max-rounds=10000 \ + --codspeed-max-time=10