diff --git a/CHANGELOG.md b/CHANGELOG.md index 57fa44b8ef..22d0ffd300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [UNRELEASED] +### Fixed + +* Avoid creating references cycles from `BoundSyncStream` and `BoundAsyncStream` to the response object. This allows memory to be freed by reference counting, which happens quickly, rather than waiting for the cyclic GC to run. + ### Removed * Drop support for Python 3.8 diff --git a/httpx/_client.py b/httpx/_client.py index 13cd933673..c4ae96ddbe 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -6,6 +6,7 @@ import time import typing import warnings +import weakref from contextlib import asynccontextmanager, contextmanager from types import TracebackType @@ -146,7 +147,7 @@ def __init__( self, stream: SyncByteStream, response: Response, start: float ) -> None: self._stream = stream - self._response = response + self._response = weakref.ref(response) self._start = start def __iter__(self) -> typing.Iterator[bytes]: @@ -155,7 +156,9 @@ def __iter__(self) -> typing.Iterator[bytes]: def close(self) -> None: elapsed = time.perf_counter() - self._start - self._response.elapsed = datetime.timedelta(seconds=elapsed) + response = self._response() + if response is not None: + response.elapsed = datetime.timedelta(seconds=elapsed) self._stream.close() @@ -169,7 +172,7 @@ def __init__( self, stream: AsyncByteStream, response: Response, start: float ) -> None: self._stream = stream - self._response = response + self._response = weakref.ref(response) self._start = start async def __aiter__(self) -> typing.AsyncIterator[bytes]: @@ -178,7 +181,9 @@ async def __aiter__(self) -> typing.AsyncIterator[bytes]: async def aclose(self) -> None: elapsed = time.perf_counter() - self._start - self._response.elapsed = datetime.timedelta(seconds=elapsed) + response = self._response() + if response is not None: + response.elapsed = datetime.timedelta(seconds=elapsed) await self._stream.aclose()