diff --git a/httpx/_client.py b/httpx/_client.py index d6a0caf085..0b67a78ddd 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -47,6 +47,7 @@ ) from ._utils import ( NetRCInfo, + Timer, URLPattern, get_environment_proxies, get_logger, @@ -811,6 +812,8 @@ def _send_single_request(self, request: Request, timeout: Timeout) -> Response: Sends a single request, without handling any redirections. """ transport = self._transport_for_url(request.url) + timer = Timer() + timer.sync_start() with map_exceptions(HTTPCORE_EXC_MAP, request=request): ( @@ -832,6 +835,7 @@ def _send_single_request(self, request: Request, timeout: Timeout) -> Response: headers=headers, stream=stream, # type: ignore request=request, + elapsed_func=timer.sync_elapsed, ) self.cookies.extract_cookies(response) @@ -1434,6 +1438,8 @@ async def _send_single_request( Sends a single request, without handling any redirections. """ transport = self._transport_for_url(request.url) + timer = Timer() + await timer.async_start() with map_exceptions(HTTPCORE_EXC_MAP, request=request): ( @@ -1455,6 +1461,7 @@ async def _send_single_request( headers=headers, stream=stream, # type: ignore request=request, + elapsed_func=timer.async_elapsed, ) self.cookies.extract_cookies(response) diff --git a/httpx/_models.py b/httpx/_models.py index 5b6a9b6571..713281e662 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -47,7 +47,6 @@ URLTypes, ) from ._utils import ( - ElapsedTimer, flatten_queryparams, guess_json_utf, is_known_encoding, @@ -606,7 +605,6 @@ def __init__( else: self.stream = encode(data, files, json) - self.timer = ElapsedTimer() self.prepare() def prepare(self) -> None: @@ -678,6 +676,7 @@ def __init__( stream: ContentStream = None, content: bytes = None, history: typing.List["Response"] = None, + elapsed_func: typing.Callable = None, ): self.status_code = status_code self.http_version = http_version @@ -688,6 +687,7 @@ def __init__( self.call_next: typing.Optional[typing.Callable] = None self.history = [] if history is None else list(history) + self._elapsed_func = elapsed_func self.is_closed = False self.is_stream_consumed = False @@ -708,7 +708,7 @@ def elapsed(self) -> datetime.timedelta: "'.elapsed' may only be accessed after the response " "has been read or closed." ) - return self._elapsed + return datetime.timedelta(seconds=self._elapsed) @property def request(self) -> Request: @@ -976,8 +976,8 @@ def close(self) -> None: """ if not self.is_closed: self.is_closed = True - if self._request is not None: - self._elapsed = self.request.timer.elapsed + if self._elapsed_func is not None: + self._elapsed = self._elapsed_func() self._raw_stream.close() async def aread(self) -> bytes: @@ -1056,8 +1056,8 @@ async def aclose(self) -> None: """ if not self.is_closed: self.is_closed = True - if self._request is not None: - self._elapsed = self.request.timer.elapsed + if self._elapsed_func is not None: + self._elapsed = await self._elapsed_func() await self._raw_stream.aclose() diff --git a/httpx/_utils.py b/httpx/_utils.py index 8080f63a46..aa670724cb 100644 --- a/httpx/_utils.py +++ b/httpx/_utils.py @@ -6,14 +6,14 @@ import os import re import sys +import time import typing import warnings -from datetime import timedelta from pathlib import Path -from time import perf_counter -from types import TracebackType from urllib.request import getproxies +import sniffio + from ._types import PrimitiveData if typing.TYPE_CHECKING: # pragma: no cover @@ -392,28 +392,35 @@ def flatten_queryparams( return items -class ElapsedTimer: - def __init__(self) -> None: - self.start: float = perf_counter() - self.end: typing.Optional[float] = None +class Timer: + async def _get_time(self) -> float: + library = sniffio.current_async_library() + if library == "trio": + import trio - def __enter__(self) -> "ElapsedTimer": - self.start = perf_counter() - return self + return trio.current_time() + elif library == "curio": # pragma: nocover + import curio - def __exit__( - self, - exc_type: typing.Type[BaseException] = None, - exc_value: BaseException = None, - traceback: TracebackType = None, - ) -> None: - self.end = perf_counter() + return await curio.clock() - @property - def elapsed(self) -> timedelta: - if self.end is None: - return timedelta(seconds=perf_counter() - self.start) - return timedelta(seconds=self.end - self.start) + import asyncio + + return asyncio.get_event_loop().time() + + def sync_start(self) -> None: + self.started = time.perf_counter() + + async def async_start(self) -> None: + self.started = await self._get_time() + + def sync_elapsed(self) -> float: + now = time.perf_counter() + return now - self.started + + async def async_elapsed(self) -> float: + now = await self._get_time() + return now - self.started class URLPattern: diff --git a/tests/models/test_responses.py b/tests/models/test_responses.py index 32163a6fc8..2b07a27040 100644 --- a/tests/models/test_responses.py +++ b/tests/models/test_responses.py @@ -1,4 +1,3 @@ -import datetime import json from unittest import mock @@ -31,7 +30,6 @@ def test_response(): assert response.text == "Hello, world!" assert response.request.method == "GET" assert response.request.url == "https://example.org" - assert response.elapsed >= datetime.timedelta(0) assert not response.is_error diff --git a/tests/test_utils.py b/tests/test_utils.py index d5dfb5819b..ae4b3aa96c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,3 @@ -import asyncio import os import random @@ -6,7 +5,6 @@ import httpx from httpx._utils import ( - ElapsedTimer, NetRCInfo, URLPattern, get_ca_bundle_from_env, @@ -177,17 +175,6 @@ def test_get_ssl_cert_file(): assert get_ca_bundle_from_env() is None -@pytest.mark.asyncio -async def test_elapsed_timer(): - with ElapsedTimer() as timer: - assert timer.elapsed.total_seconds() == pytest.approx(0, abs=0.05) - await asyncio.sleep(0.1) - await asyncio.sleep( - 0.1 - ) # test to ensure time spent after timer exits isn't accounted for. - assert timer.elapsed.total_seconds() == pytest.approx(0.1, abs=0.05) - - @pytest.mark.parametrize( ["environment", "proxies"], [