From 5f5729d2db02c5643e2e4e33d840b623a1af9c48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Nov 2024 10:35:16 -0800 Subject: [PATCH] Defer creation of cookies for client responses until needed (#10029) --- CHANGES/10029.misc.rst | 1 + aiohttp/client.py | 2 +- aiohttp/client_reqrep.py | 25 +++++++++++++++++++------ tests/test_client_response.py | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 CHANGES/10029.misc.rst diff --git a/CHANGES/10029.misc.rst b/CHANGES/10029.misc.rst new file mode 100644 index 00000000000..d98729ecac8 --- /dev/null +++ b/CHANGES/10029.misc.rst @@ -0,0 +1 @@ +Improved performance of creating :class:`aiohttp.ClientResponse` objects when there are no cookies -- by :user:`bdraco`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 1485356c3a1..16c8c749ea0 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -687,7 +687,7 @@ async def _request( raise raise ClientOSError(*exc.args) from exc - if cookies := resp.cookies: + if cookies := resp._cookies: self._cookie_jar.update_cookies(cookies, resp.url) # redirects diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 1ca93a2bf7a..08a4d44b345 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -766,6 +766,7 @@ class ClientResponse(HeadersMixin): _raw_headers: RawHeaders = None # type: ignore[assignment] _connection: Optional["Connection"] = None # current connection + _cookies: Optional[SimpleCookie] = None _continue: Optional["asyncio.Future[bool]"] = None _source_traceback: Optional[traceback.StackSummary] = None _session: Optional["ClientSession"] = None @@ -797,7 +798,6 @@ def __init__( super().__init__() self.method = method - self.cookies = SimpleCookie() self._real_url = url self._url = url.with_fragment(None) if url.raw_fragment else url @@ -846,6 +846,16 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None: else: writer.add_done_callback(self.__reset_writer) + @property + def cookies(self) -> SimpleCookie: + if self._cookies is None: + self._cookies = SimpleCookie() + return self._cookies + + @cookies.setter + def cookies(self, cookies: SimpleCookie) -> None: + self._cookies = cookies + @reify def url(self) -> URL: return self._url @@ -1005,11 +1015,14 @@ async def start(self, connection: "Connection") -> "ClientResponse": self.content = payload # cookies - for hdr in self.headers.getall(hdrs.SET_COOKIE, ()): - try: - self.cookies.load(hdr) - except CookieError as exc: - client_logger.warning("Can not load response cookies: %s", exc) + if cookie_hdrs := self.headers.getall(hdrs.SET_COOKIE, ()): + cookies = SimpleCookie() + for hdr in cookie_hdrs: + try: + cookies.load(hdr) + except CookieError as exc: + client_logger.warning("Can not load response cookies: %s", exc) + self._cookies = cookies return self def _response_eof(self) -> None: diff --git a/tests/test_client_response.py b/tests/test_client_response.py index 4ac96de2608..75d3ee0d4b3 100644 --- a/tests/test_client_response.py +++ b/tests/test_client_response.py @@ -1179,6 +1179,25 @@ def side_effect(*args: object, **kwargs: object) -> "asyncio.Future[bytes]": ) +def test_response_cookies( + loop: asyncio.AbstractEventLoop, session: ClientSession +) -> None: + response = ClientResponse( + "get", + URL("http://python.org"), + request_info=mock.Mock(), + writer=WriterMock(), + continue100=None, + timer=TimerNoop(), + traces=[], + loop=loop, + session=session, + ) + cookies = response.cookies + # Ensure the same cookies object is returned each time + assert response.cookies is cookies + + def test_response_real_url( loop: asyncio.AbstractEventLoop, session: ClientSession ) -> None: