From f41df8e5496dfe4dced05eb4e570517bfdbe37a7 Mon Sep 17 00:00:00 2001 From: Sean Doherty Date: Sun, 17 May 2026 01:38:52 -0500 Subject: [PATCH 1/2] Avoid expensive cookie truthiness checks --- src/httpx2/httpx2/_client.py | 14 +++++++++----- src/httpx2/httpx2/_models.py | 7 +++++++ tests/httpx2/models/test_cookies.py | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/httpx2/httpx2/_client.py b/src/httpx2/httpx2/_client.py index f7535d33..fd246738 100644 --- a/src/httpx2/httpx2/_client.py +++ b/src/httpx2/httpx2/_client.py @@ -396,11 +396,15 @@ def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | No Merge a cookies argument together with any cookies on the client, to create the cookies used for the outgoing request. """ - if cookies or self.cookies: - merged_cookies = Cookies(self.cookies) - merged_cookies.update(cookies) - return merged_cookies - return cookies + if cookies is None: + return Cookies(self.cookies) if self.cookies else None + + if not self.cookies: + return Cookies(cookies) + + merged_cookies = Cookies(self.cookies) + merged_cookies.update(cookies) + return merged_cookies def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None: """ diff --git a/src/httpx2/httpx2/_models.py b/src/httpx2/httpx2/_models.py index e6aeabd6..4cd9be82 100644 --- a/src/httpx2/httpx2/_models.py +++ b/src/httpx2/httpx2/_models.py @@ -1190,6 +1190,13 @@ def __iter__(self) -> typing.Iterator[str]: return (cookie.name for cookie in self.jar) def __bool__(self) -> bool: + cookie_store = getattr(self.jar, "_cookies", None) + if cookie_store is not None: + cookie_store = typing.cast(dict[str, dict[str, dict[str, Cookie]]], cookie_store) + return any( + path_cookies for domain_cookies in cookie_store.values() for path_cookies in domain_cookies.values() + ) + for _ in self.jar: return True return False diff --git a/tests/httpx2/models/test_cookies.py b/tests/httpx2/models/test_cookies.py index 0ca1e38f..b572a52c 100644 --- a/tests/httpx2/models/test_cookies.py +++ b/tests/httpx2/models/test_cookies.py @@ -5,6 +5,11 @@ import httpx2 +class NonIterableCookieJar(http.cookiejar.CookieJar): + def __iter__(self): + raise AssertionError("CookieJar.__iter__ should not be used for truthiness") + + def test_cookies(): cookies = httpx2.Cookies({"name": "value"}) assert cookies["name"] == "value" @@ -20,6 +25,17 @@ def test_cookies(): assert bool(cookies) is False +def test_cookies_bool_does_not_iterate_cookie_jar(): + jar = NonIterableCookieJar() + cookies = httpx2.Cookies(jar) + + assert bool(cookies) is False + + cookies.set("name", "value") + + assert bool(cookies) is True + + def test_cookies_update(): cookies = httpx2.Cookies() more_cookies = httpx2.Cookies() From cb1bf24ded39930f863975dab421252301fca09e Mon Sep 17 00:00:00 2001 From: Sean Doherty Date: Sun, 17 May 2026 01:51:29 -0500 Subject: [PATCH 2/2] Cover optimized cookie branches --- tests/httpx2/client/test_cookies.py | 19 +++++++++++++ tests/httpx2/models/test_cookies.py | 42 +++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/tests/httpx2/client/test_cookies.py b/tests/httpx2/client/test_cookies.py index b95fcff3..93e5e6e4 100644 --- a/tests/httpx2/client/test_cookies.py +++ b/tests/httpx2/client/test_cookies.py @@ -45,6 +45,25 @@ def test_set_per_request_cookie_is_deprecated() -> None: assert response.json() == {"cookies": "example-name=example-value"} +def test_set_per_request_cookie_merges_with_client_cookies() -> None: + url = "http://example.org/echo_cookies" + cookies = {"request-name": "request-value"} + + client = httpx2.Client( + cookies={"client-name": "client-value"}, + transport=httpx2.MockTransport(get_and_set_cookies), + ) + with pytest.warns(DeprecationWarning): + response = client.get(url, cookies=cookies) + + assert response.status_code == 200 + cookie_header = response.json()["cookies"] + assert set(cookie_header.split("; ")) == { + "client-name=client-value", + "request-name=request-value", + } + + def test_set_cookie_with_cookiejar() -> None: """ Send a request including a cookie, using a `CookieJar` instance. diff --git a/tests/httpx2/models/test_cookies.py b/tests/httpx2/models/test_cookies.py index b572a52c..640c4fa5 100644 --- a/tests/httpx2/models/test_cookies.py +++ b/tests/httpx2/models/test_cookies.py @@ -1,4 +1,5 @@ import http +from collections.abc import Iterator import pytest @@ -6,8 +7,40 @@ class NonIterableCookieJar(http.cookiejar.CookieJar): - def __iter__(self): - raise AssertionError("CookieJar.__iter__ should not be used for truthiness") + def __iter__(self) -> Iterator[http.cookiejar.Cookie]: + raise AssertionError("CookieJar.__iter__ should not be used for truthiness") # pragma: no cover + + +class IterableOnlyCookieJar(http.cookiejar.CookieJar): + def __init__(self, cookies: list[http.cookiejar.Cookie]) -> None: + super().__init__() + self._iter_cookies = cookies + delattr(self, "_cookies") + + def __iter__(self) -> Iterator[http.cookiejar.Cookie]: + return iter(self._iter_cookies) + + +def make_cookie(name: str = "name", value: str = "value") -> http.cookiejar.Cookie: + return http.cookiejar.Cookie( + version=0, + name=name, + value=value, + port=None, + port_specified=False, + domain="", + domain_specified=False, + domain_initial_dot=False, + path="/", + path_specified=True, + secure=False, + expires=None, + discard=True, + comment=None, + comment_url=None, + rest={"HttpOnly": ""}, + rfc2109=False, + ) def test_cookies(): @@ -36,6 +69,11 @@ def test_cookies_bool_does_not_iterate_cookie_jar(): assert bool(cookies) is True +def test_cookies_bool_iterates_custom_cookie_jar_without_cookie_store(): + assert bool(httpx2.Cookies(IterableOnlyCookieJar([]))) is False + assert bool(httpx2.Cookies(IterableOnlyCookieJar([make_cookie()]))) is True + + def test_cookies_update(): cookies = httpx2.Cookies() more_cookies = httpx2.Cookies()