Skip to content

Commit 7f4057a

Browse files
committed
perf: skip the URL query merge when there is no query or no params
1 parent 96e9a02 commit 7f4057a

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

‎src/httpware/client.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,8 @@ def _build_request(
390390
client: httpx2.Client | httpx2.AsyncClient, method: str, url: httpx2.URL | str, kwargs: dict[str, typing.Any]
391391
) -> httpx2.Request:
392392
"""Build via `client`, keeping the URL's own query bytes ahead of any params, which httpx2 would replace."""
393+
if (isinstance(url, str) and "?" not in url) or (kwargs.get("params") is None and not client.params):
394+
return client.build_request(method, url, **kwargs)
393395
parsed = httpx2.URL(url)
394396
if not parsed.query:
395397
return client.build_request(method, url, **kwargs)

‎tests/test_url_query_fast_path.py‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""URLs that skip the query merge, and boundary URLs that must not skip it, build the expected request."""
2+
3+
from http import HTTPStatus
4+
5+
import httpx2
6+
import pytest
7+
8+
from httpware import AsyncClient, Client
9+
10+
11+
def _ok(request: httpx2.Request) -> httpx2.Response:
12+
return httpx2.Response(HTTPStatus.OK, request=request)
13+
14+
15+
_CASES = [
16+
pytest.param("https://example.test/x", None, {}, "https://example.test/x", id="no-query-no-params"),
17+
pytest.param("https://example.test/x", {"b": "2"}, {"c": "3"}, "https://example.test/x?c=3&b=2", id="no-query"),
18+
pytest.param("https://example.test/x?a=1", None, {}, "https://example.test/x?a=1", id="query-without-params"),
19+
pytest.param(
20+
"https://example.test/x?a=1", {}, {}, "https://example.test/x?a=1", id="query-with-empty-params-not-skipped"
21+
),
22+
pytest.param(
23+
"https://example.test/x?a=1", None, {"c": "3"}, "https://example.test/x?a=1&c=3", id="client-params-not-skipped"
24+
),
25+
pytest.param(
26+
"https://example.test/x#f?a=1",
27+
{"b": "2"},
28+
{},
29+
"https://example.test/x?b=2#f?a=1",
30+
id="question-mark-in-fragment",
31+
),
32+
pytest.param(
33+
"https://example.test/x?a=1#f", {"b": "2"}, {}, "https://example.test/x?a=1&b=2#f", id="query-and-fragment"
34+
),
35+
pytest.param("https://example.test/x?", {"b": "2"}, {}, "https://example.test/x?b=2", id="empty-query"),
36+
pytest.param(
37+
httpx2.URL("https://example.test/x?a=1"),
38+
{"b": "2"},
39+
{},
40+
"https://example.test/x?a=1&b=2",
41+
id="url-instance-with-query-not-skipped",
42+
),
43+
pytest.param(
44+
httpx2.URL("https://example.test/x?a=1"), None, {}, "https://example.test/x?a=1", id="url-instance-no-params"
45+
),
46+
pytest.param(
47+
httpx2.URL("https://example.test/x"), {"b": "2"}, {}, "https://example.test/x?b=2", id="url-instance-no-query"
48+
),
49+
]
50+
51+
52+
@pytest.mark.parametrize(("url", "params", "client_params", "expected"), _CASES)
53+
def test_async_build_request_url(
54+
url: str, params: dict[str, str] | None, client_params: dict[str, str], expected: str
55+
) -> None:
56+
client = AsyncClient(httpx2_client=httpx2.AsyncClient(params=client_params))
57+
assert str(client.build_request("GET", url, params=params).url) == expected
58+
59+
60+
@pytest.mark.parametrize(("url", "params", "client_params", "expected"), _CASES)
61+
def test_sync_build_request_url(
62+
url: str, params: dict[str, str] | None, client_params: dict[str, str], expected: str
63+
) -> None:
64+
client = Client(httpx2_client=httpx2.Client(params=client_params))
65+
assert str(client.build_request("GET", url, params=params).url) == expected
66+
67+
68+
_BASE_URL_CASES = [
69+
pytest.param("items", None, {}, "https://example.test/api/items", id="relative-no-query"),
70+
pytest.param("items?a=1", None, {}, "https://example.test/api/items?a=1", id="relative-query-without-params"),
71+
pytest.param("/items?a=1", {"b": "2"}, {}, "https://example.test/api/items?a=1&b=2", id="relative-query-params"),
72+
pytest.param(
73+
"items?a=1", None, {"c": "3"}, "https://example.test/api/items?a=1&c=3", id="relative-query-client-params"
74+
),
75+
]
76+
77+
78+
@pytest.mark.parametrize(("url", "params", "client_params", "expected"), _BASE_URL_CASES)
79+
async def test_async_get_joins_base_url(
80+
url: str, params: dict[str, str] | None, client_params: dict[str, str], expected: str
81+
) -> None:
82+
captured: list[httpx2.Request] = []
83+
84+
def handler(request: httpx2.Request) -> httpx2.Response:
85+
captured.append(request)
86+
return _ok(request)
87+
88+
client = AsyncClient(
89+
base_url="https://example.test/api", params=client_params, transport=httpx2.MockTransport(handler)
90+
)
91+
await client.get(url, params=params)
92+
assert str(captured[0].url) == expected
93+
94+
95+
@pytest.mark.parametrize(("url", "params", "client_params", "expected"), _BASE_URL_CASES)
96+
def test_sync_get_joins_base_url(
97+
url: str, params: dict[str, str] | None, client_params: dict[str, str], expected: str
98+
) -> None:
99+
captured: list[httpx2.Request] = []
100+
101+
def handler(request: httpx2.Request) -> httpx2.Response:
102+
captured.append(request)
103+
return _ok(request)
104+
105+
client = Client(base_url="https://example.test/api", params=client_params, transport=httpx2.MockTransport(handler))
106+
client.get(url, params=params)
107+
assert str(captured[0].url) == expected

0 commit comments

Comments
 (0)