diff --git a/pyproject.toml b/pyproject.toml index 02a3820fc..df32ce2a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,6 +81,7 @@ filterwarnings = [ "error", "ignore: run_until_first_complete is deprecated and will be removed in a future version.:DeprecationWarning", "ignore: starlette.middleware.wsgi is deprecated and will be removed in a future release.*:DeprecationWarning", + "ignore: The `timeout` argument doesn't work, is deprecated, and will be removed in future versions.*:DeprecationWarning", "ignore: Async generator 'starlette.requests.Request.stream' was garbage collected before it had been exhausted.*:ResourceWarning", "ignore: Use 'content=<...>' to upload raw bytes/text content.:DeprecationWarning", ] diff --git a/starlette/testclient.py b/starlette/testclient.py index fb2ad6e35..2fe9844fc 100644 --- a/starlette/testclient.py +++ b/starlette/testclient.py @@ -7,6 +7,7 @@ import math import sys import typing +import warnings from concurrent.futures import Future from types import GeneratorType from urllib.parse import unquote, urljoin @@ -409,6 +410,17 @@ def _portal_factory(self) -> typing.Generator[anyio.abc.BlockingPortal, None, No with anyio.from_thread.start_blocking_portal(**self.async_backend) as portal: yield portal + def _handle_timeout( + self, + timeout: httpx._types.TimeoutTypes | None, + ) -> httpx._types.TimeoutTypes | httpx._client.UseClientDefault: + default_timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT + if timeout is not None: + message = "The `timeout` argument doesn't work, is deprecated, and will be removed in future versions." + warnings.warn(message, DeprecationWarning) + return timeout + return default_timeout + def request( # type: ignore[override] self, method: str, @@ -423,9 +435,10 @@ def request( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) url = self._merge_url(url) return super().request( method, @@ -439,7 +452,7 @@ def request( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -452,9 +465,10 @@ def get( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().get( url, params=params, @@ -462,7 +476,7 @@ def get( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -475,9 +489,10 @@ def options( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().options( url, params=params, @@ -485,7 +500,7 @@ def options( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -498,9 +513,10 @@ def head( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().head( url, params=params, @@ -508,7 +524,7 @@ def head( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -525,9 +541,10 @@ def post( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().post( url, content=content, @@ -539,7 +556,7 @@ def post( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -556,9 +573,10 @@ def put( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().put( url, content=content, @@ -570,7 +588,7 @@ def put( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -587,9 +605,10 @@ def patch( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().patch( url, content=content, @@ -601,7 +620,7 @@ def patch( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) @@ -614,9 +633,10 @@ def delete( # type: ignore[override] cookies: httpx._types.CookieTypes | None = None, auth: httpx._types.AuthTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, follow_redirects: bool | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, + timeout: httpx._types.TimeoutTypes | None = None, extensions: dict[str, typing.Any] | None = None, ) -> httpx.Response: + _timeout = self._handle_timeout(timeout) return super().delete( url, params=params, @@ -624,7 +644,7 @@ def delete( # type: ignore[override] cookies=cookies, auth=auth, follow_redirects=follow_redirects, - timeout=timeout, + timeout=_timeout, extensions=extensions, ) diff --git a/tests/test_testclient.py b/tests/test_testclient.py index 478dbca46..a9b0f2482 100644 --- a/tests/test_testclient.py +++ b/tests/test_testclient.py @@ -422,3 +422,22 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None: with client.websocket_connect("/hello-world", params={"foo": "bar"}) as websocket: data = websocket.receive_bytes() assert data == b"/hello-world" + + +@pytest.mark.parametrize("method", [None, "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]) +def test_warning_timeout_deprecation(test_client_factory: TestClientFactory, method: str | None) -> None: + def homepage(request: Request) -> Response: + return Response("Hello, world!") + + allowed_method = [method] if method else None + app = Starlette(routes=[Route("/", endpoint=homepage, methods=allowed_method)]) + client = test_client_factory(app) + with pytest.warns( + DeprecationWarning, + match="The `timeout` argument doesn't work, is deprecated, and will be removed in future versions.", + ): + if method is None: + client.request("GET", "/", timeout=0.1) + else: + client_method = getattr(client, method.lower()) + client_method("/", timeout=0.1)