diff --git a/src/httpx2/httpx2/_client.py b/src/httpx2/httpx2/_client.py index f7535d33..e0ce82ed 100644 --- a/src/httpx2/httpx2/_client.py +++ b/src/httpx2/httpx2/_client.py @@ -132,43 +132,43 @@ class ClientState(enum.Enum): class BoundSyncStream(SyncByteStream): """ - A byte stream that is bound to a given response instance, and that - ensures the `response.elapsed` is set once the response is closed. + A byte stream that tracks elapsed time for a response. Once closed, the + elapsed time is available via the `elapsed` attribute, and the response + can read it back from `response.stream.elapsed`. """ - def __init__(self, stream: SyncByteStream, response: Response, start: float) -> None: + def __init__(self, stream: SyncByteStream, start: float) -> None: self._stream = stream - self._response = response self._start = start + self.elapsed: datetime.timedelta | None = None def __iter__(self) -> typing.Iterator[bytes]: for chunk in self._stream: yield chunk def close(self) -> None: - elapsed = time.perf_counter() - self._start - self._response.elapsed = datetime.timedelta(seconds=elapsed) + self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start) self._stream.close() class BoundAsyncStream(AsyncByteStream): """ - An async byte stream that is bound to a given response instance, and that - ensures the `response.elapsed` is set once the response is closed. + An async byte stream that tracks elapsed time for a response. Once closed, + the elapsed time is available via the `elapsed` attribute, and the response + can read it back from `response.stream.elapsed`. """ - def __init__(self, stream: AsyncByteStream, response: Response, start: float) -> None: + def __init__(self, stream: AsyncByteStream, start: float) -> None: self._stream = stream - self._response = response self._start = start + self.elapsed: datetime.timedelta | None = None async def __aiter__(self) -> typing.AsyncIterator[bytes]: async for chunk in self._stream: yield chunk async def aclose(self) -> None: - elapsed = time.perf_counter() - self._start - self._response.elapsed = datetime.timedelta(seconds=elapsed) + self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start) await self._stream.aclose() @@ -977,7 +977,7 @@ def _send_single_request(self, request: Request) -> Response: assert isinstance(response.stream, SyncByteStream) response.request = request - response.stream = BoundSyncStream(response.stream, response=response, start=start) + response.stream = BoundSyncStream(response.stream, start=start) self.cookies.extract_cookies(response) response.default_encoding = self._default_encoding @@ -1680,7 +1680,7 @@ async def _send_single_request(self, request: Request) -> Response: assert isinstance(response.stream, AsyncByteStream) response.request = request - response.stream = BoundAsyncStream(response.stream, response=response, start=start) + response.stream = BoundAsyncStream(response.stream, start=start) self.cookies.extract_cookies(response) response.default_encoding = self._default_encoding diff --git a/src/httpx2/httpx2/_models.py b/src/httpx2/httpx2/_models.py index e6aeabd6..834f0806 100644 --- a/src/httpx2/httpx2/_models.py +++ b/src/httpx2/httpx2/_models.py @@ -563,6 +563,9 @@ def elapsed(self) -> datetime.timedelta: cycle to complete. """ if not hasattr(self, "_elapsed"): + stream_elapsed: datetime.timedelta | None = getattr(self.stream, "elapsed", None) + if stream_elapsed is not None: + return stream_elapsed raise RuntimeError("'.elapsed' may only be accessed after the response has been read or closed.") return self._elapsed diff --git a/tests/httpx2/models/test_responses.py b/tests/httpx2/models/test_responses.py index 860d99c1..5a9fbbbd 100644 --- a/tests/httpx2/models/test_responses.py +++ b/tests/httpx2/models/test_responses.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime import json import pickle import typing @@ -771,6 +772,12 @@ async def test_elapsed_not_available_until_closed() -> None: response.elapsed # noqa: B018 +def test_elapsed_set_directly() -> None: + response = httpx2.Response(200) + response.elapsed = datetime.timedelta(seconds=1) + assert response.elapsed == datetime.timedelta(seconds=1) + + def test_unknown_status_code() -> None: response = httpx2.Response( 600,