Skip to content
Open
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
36 changes: 36 additions & 0 deletions .github/workflows/codspeed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CodSpeed

on:
push:
branches: ["main"]
pull_request:
branches: ["main", "version-*"]
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:

permissions:
contents: read
id-token: write

jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

Check notice

Code scanning / zizmor

credential persistence through GitHub Actions artifacts Note

credential persistence through GitHub Actions artifacts

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: "3.13"
enable-cache: true

- name: Install dependencies
run: uv sync --group dev

- name: Run benchmarks
uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1
with:
mode: simulation
run: uv run pytest tests/benchmarks/ --codspeed
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<p align="center">
<a href="https://github.com/pydantic/httpx2/actions"><img src="https://github.com/pydantic/httpx2/workflows/Test%20Suite/badge.svg" alt="Test Suite"></a>
<a href="https://pypi.org/project/httpx2/"><img src="https://badge.fury.io/py/httpx2.svg" alt="Package version"></a>
<a href="https://codspeed.io/AvalancheHQ/httpx2?utm_source=badge"><img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed"/></a>
</p>

HTTPX2 is a fully featured HTTP client library for Python 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dev = [
# Packaging
"build==1.3.0",
"twine==6.1.0",
"pytest-codspeed>=4.5.0",
]
docs = ["zensical>=0.0.41", "mkdocstrings[python]>=0.27"]
bench = [
Expand Down
Empty file added tests/benchmarks/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions tests/benchmarks/test_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Benchmarks for request/response content encoding."""

from httpx2._content import encode_content, encode_json, encode_urlencoded_data


def test_encode_content_bytes_small(benchmark):
"""Encode a small bytes payload."""
benchmark(encode_content, b"Hello, world!")


def test_encode_content_bytes_medium(benchmark):
"""Encode a 10 KB bytes payload."""
payload = b"x" * 10_240
benchmark(encode_content, payload)


def test_encode_content_str(benchmark):
"""Encode a string payload (triggers UTF-8 encoding)."""
benchmark(encode_content, "Hello, world! " * 100)


def test_encode_urlencoded_data(benchmark):
"""Encode URL-encoded form data."""
data = {
"username": "admin",
"password": "secret",
"remember": "true",
"redirect": "/dashboard",
}
benchmark(encode_urlencoded_data, data)


def test_encode_json_small(benchmark):
"""Encode a small JSON body."""
benchmark(encode_json, {"key": "value"})


def test_encode_json_nested(benchmark):
"""Encode a nested JSON body."""
payload = {
"users": [{"id": i, "name": f"user_{i}", "email": f"user_{i}@example.com"} for i in range(20)],
"meta": {"page": 1, "total": 100},
}
benchmark(encode_json, payload)
48 changes: 48 additions & 0 deletions tests/benchmarks/test_decoders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Benchmarks for response decoders."""

from httpx2._decoders import ByteChunker, IdentityDecoder, LineDecoder, TextDecoder


def test_identity_decoder(benchmark):
"""Decode a payload through the identity (no-op) decoder."""
decoder = IdentityDecoder()
data = b"Hello, world!" * 100
benchmark(decoder.decode, data)


def test_text_decoder(benchmark):
"""Decode bytes to text using TextDecoder."""
data = b"Hello, world! This is a test of the text decoder. " * 50

def run():
decoder = TextDecoder(encoding="utf-8")
decoder.decode(data)
decoder.flush()

benchmark(run)


def test_line_decoder(benchmark):
"""Split text into lines using LineDecoder."""
text = "line1\nline2\nline3\nline4\nline5\n" * 20

def run():
decoder = LineDecoder()
lines = list(decoder.decode(text))
lines.extend(decoder.flush())
return lines

benchmark(run)


def test_byte_chunker(benchmark):
"""Chunk bytes into fixed-size pieces."""
data = b"x" * 10_000

def run():
chunker = ByteChunker(chunk_size=1024)
chunks = list(chunker.decode(data))
chunks.extend(chunker.flush())
return chunks

benchmark(run)
115 changes: 115 additions & 0 deletions tests/benchmarks/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Benchmarks for Headers, Request, and Response construction."""

from httpx2 import Headers, Request, Response
from httpx2._urls import URL

# ---------------------------------------------------------------------------
# Headers
# ---------------------------------------------------------------------------


HEADER_LIST = [
("Content-Type", "application/json"),
("Content-Length", "1024"),
("Authorization", "Bearer token123"),
("Accept", "text/html"),
("Accept-Encoding", "gzip, deflate, br"),
("Cache-Control", "no-cache"),
("X-Request-ID", "abc-def-123"),
("X-Custom-Header", "custom-value"),
]


def test_headers_from_list(benchmark):
"""Construct Headers from a list of tuples."""
benchmark(Headers, HEADER_LIST)


def test_headers_getitem(benchmark):
"""Look up a header value by name (case-insensitive)."""
headers = Headers(HEADER_LIST)
benchmark(headers.__getitem__, "content-type")


def test_headers_contains(benchmark):
"""Check header presence."""
headers = Headers(HEADER_LIST)
benchmark(headers.__contains__, "Authorization")


def test_headers_items(benchmark):
"""Iterate over all header items."""
headers = Headers(HEADER_LIST)
benchmark(lambda: dict(headers.items()))


def test_headers_copy(benchmark):
"""Copy a Headers instance."""
headers = Headers(HEADER_LIST)
benchmark(headers.copy)


# ---------------------------------------------------------------------------
# URL
# ---------------------------------------------------------------------------


def test_url_construction(benchmark):
"""Build a URL object from a string."""
benchmark(URL, "https://www.example.com/path?query=value#fragment")


def test_url_copy_with(benchmark):
"""Copy a URL, replacing the path component."""
url = URL("https://www.example.com/path?query=value#fragment")
benchmark(url.copy_with, path="/new-path")


# ---------------------------------------------------------------------------
# Request
# ---------------------------------------------------------------------------


def test_request_get(benchmark):
"""Construct a minimal GET request."""
benchmark(Request, "GET", "https://example.com/")


def test_request_post_json(benchmark):
"""Construct a POST request with a JSON body."""
benchmark(Request, "POST", "https://example.com/api", json={"key": "value", "count": 42})


def test_request_post_form(benchmark):
"""Construct a POST request with form-encoded data."""
benchmark(Request, "POST", "https://example.com/api", data={"username": "admin", "password": "secret"})


def test_request_with_headers(benchmark):
"""Construct a GET request with custom headers."""
benchmark(
Request,
"GET",
"https://example.com/",
headers={"Authorization": "Bearer token", "Accept": "application/json"},
)


# ---------------------------------------------------------------------------
# Response
# ---------------------------------------------------------------------------


def test_response_text(benchmark):
"""Construct and read a plain-text Response."""
benchmark(Response, 200, text="Hello, world!")


def test_response_json(benchmark):
"""Construct and read a JSON Response."""
benchmark(Response, 200, json={"message": "ok", "data": [1, 2, 3]})


def test_response_html(benchmark):
"""Construct and read an HTML Response."""
benchmark(Response, 200, html="<html><body><h1>Hello</h1></body></html>")
38 changes: 38 additions & 0 deletions tests/benchmarks/test_urlparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Benchmarks for URL parsing — one of the hottest paths in httpx2."""

from httpx2._urlparse import urlparse


def test_urlparse_simple(benchmark):
"""Parse a simple HTTP URL."""
benchmark(urlparse, "https://www.example.com/path")


def test_urlparse_with_query_and_fragment(benchmark):
"""Parse a URL containing query parameters and a fragment."""
benchmark(urlparse, "https://www.example.com/path?key=value&foo=bar#section")


def test_urlparse_complex(benchmark):
"""Parse a URL with userinfo, port, long path, query, and fragment."""
benchmark(urlparse, "https://user:password@www.example.com:8443/path/to/resource?query=value&page=1#fragment")


def test_urlparse_ipv6(benchmark):
"""Parse a URL with an IPv6 host."""
benchmark(urlparse, "https://[::1]:8080/path")


def test_urlparse_unicode_host(benchmark):
"""Parse a URL with a unicode (IDNA) hostname."""
benchmark(urlparse, "https://münchen.de/path")


def test_urlparse_long_path(benchmark):
"""Parse a URL with a deeply nested path."""
benchmark(urlparse, "https://example.com/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p")


def test_urlparse_with_encoded_characters(benchmark):
"""Parse a URL containing percent-encoded characters."""
benchmark(urlparse, "https://example.com/path%20with%20spaces?q=hello%20world")
33 changes: 33 additions & 0 deletions uv.lock

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

Loading