From 830ee04037272a7a0c930d120829ae81bea313de Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Mon, 21 Nov 2022 17:31:53 +0000 Subject: [PATCH 1/7] Enable mypy option `--disallow-any-generics` --- httpx/_auth.py | 5 ++++- httpx/_compat.py | 11 +---------- httpx/_main.py | 2 +- httpx/_models.py | 2 +- httpx/_transports/asgi.py | 15 ++++++++++++--- httpx/_transports/mock.py | 4 ++-- httpx/_transports/wsgi.py | 21 +++++++++++++++------ httpx/_urls.py | 6 +++--- httpx/_utils.py | 2 +- setup.cfg | 1 + tests/client/test_auth.py | 4 ++-- 11 files changed, 43 insertions(+), 30 deletions(-) diff --git a/httpx/_auth.py b/httpx/_auth.py index 2b00b49d17..0f54be9b40 100644 --- a/httpx/_auth.py +++ b/httpx/_auth.py @@ -10,6 +10,9 @@ from ._models import Request, Response from ._utils import to_bytes, to_str, unquote +if typing.TYPE_CHECKING: # pragma: no cover + from hashlib import _Hash + class Auth: """ @@ -139,7 +142,7 @@ def _build_auth_header( class DigestAuth(Auth): - _ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable] = { + _ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable[[bytes], "_Hash"]] = { "MD5": hashlib.md5, "MD5-SESS": hashlib.md5, "SHA": hashlib.sha1, diff --git a/httpx/_compat.py b/httpx/_compat.py index 533fab46cf..f8f7c5fb64 100644 --- a/httpx/_compat.py +++ b/httpx/_compat.py @@ -2,13 +2,9 @@ The _compat module is used for code which requires branching between different Python environments. It is excluded from the code coverage checks. """ -import asyncio + import ssl import sys -import typing - -if typing.TYPE_CHECKING: # pragma: no cover - import typing_extensions # Brotli support is optional # The C bindings in `brotli` are recommended for CPython. @@ -43,8 +39,3 @@ def set_minimum_tls_version_1_2(context: ssl.SSLContext) -> None: context.options |= ssl.OP_NO_SSLv3 context.options |= ssl.OP_NO_TLSv1 context.options |= ssl.OP_NO_TLSv1_1 - - -def iscoroutine(coro: object) -> "typing_extensions.TypeGuard[typing.Coroutine]": - # Drop when this is resolved: https://github.com/python/typeshed/pull/8104 - return asyncio.iscoroutine(coro) diff --git a/httpx/_main.py b/httpx/_main.py index 24f1e558b8..99ee2ac50b 100644 --- a/httpx/_main.py +++ b/httpx/_main.py @@ -179,7 +179,7 @@ def print_response(response: Response) -> None: console.print(f"<{len(response.content)} bytes of binary data>") -def format_certificate(cert: dict) -> str: # pragma: nocover +def format_certificate(cert: typing.Dict[str, typing.Any]) -> str: # pragma: nocover lines = [] for key, value in cert.items(): if isinstance(value, (list, tuple)): diff --git a/httpx/_models.py b/httpx/_models.py index 5d8a51b3bf..4a9e4a4960 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -1002,7 +1002,7 @@ async def aclose(self) -> None: await self.stream.aclose() -class Cookies(MutableMapping): +class Cookies(MutableMapping[str, str]): """ HTTP Cookies, as a mutable mapping. """ diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py index 711a6f6ce7..ef8250f7af 100644 --- a/httpx/_transports/asgi.py +++ b/httpx/_transports/asgi.py @@ -13,6 +13,15 @@ Event = typing.Union[asyncio.Event, trio.Event] + # Taken directly from starlette.types + Scope = typing.MutableMapping[str, typing.Any] + Message = typing.MutableMapping[str, typing.Any] + + Receive = typing.Callable[[], typing.Awaitable[Message]] + Send = typing.Callable[[Message], typing.Awaitable[None]] + + ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]] + def create_event() -> "Event": if sniffio.current_async_library() == "trio": @@ -68,7 +77,7 @@ class ASGITransport(AsyncBaseTransport): def __init__( self, - app: typing.Callable, + app: "ASGIApp", raise_app_exceptions: bool = True, root_path: str = "", client: typing.Tuple[str, int] = ("127.0.0.1", 123), @@ -113,7 +122,7 @@ async def handle_async_request( # ASGI callables. - async def receive() -> dict: + async def receive() -> Message: nonlocal request_complete if request_complete: @@ -127,7 +136,7 @@ async def receive() -> dict: return {"type": "http.request", "body": b"", "more_body": False} return {"type": "http.request", "body": body, "more_body": True} - async def send(message: dict) -> None: + async def send(message: Message) -> None: nonlocal status_code, response_headers, response_started if message["type"] == "http.response.start": diff --git a/httpx/_transports/mock.py b/httpx/_transports/mock.py index 4ff4913df0..765a109456 100644 --- a/httpx/_transports/mock.py +++ b/httpx/_transports/mock.py @@ -1,12 +1,12 @@ import typing +from asyncio import iscoroutine -from .._compat import iscoroutine from .._models import Request, Response from .base import AsyncBaseTransport, BaseTransport class MockTransport(AsyncBaseTransport, BaseTransport): - def __init__(self, handler: typing.Callable) -> None: + def __init__(self, handler: typing.Callable[[Request], Response]) -> None: self.handler = handler def handle_request( diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py index 3dedf49f96..bf6978e330 100644 --- a/httpx/_transports/wsgi.py +++ b/httpx/_transports/wsgi.py @@ -7,8 +7,14 @@ from .._types import SyncByteStream from .base import BaseTransport +if typing.TYPE_CHECKING: + from _typeshed import OptExcInfo + from _typeshed.wsgi import WSGIApplication -def _skip_leading_empty_chunks(body: typing.Iterable) -> typing.Iterable: + _T = typing.TypeVar("_T") + + +def _skip_leading_empty_chunks(body: typing.Iterable["_T"]) -> typing.Iterable["_T"]: body = iter(body) for chunk in body: if chunk: @@ -65,7 +71,7 @@ class WSGITransport(BaseTransport): def __init__( self, - app: typing.Callable, + app: "WSGIApplication", raise_app_exceptions: bool = True, script_name: str = "", remote_addr: str = "127.0.0.1", @@ -106,15 +112,18 @@ def handle_request(self, request: Request) -> Response: seen_status = None seen_response_headers = None - seen_exc_info = None + seen_exc_info: typing.Optional[OptExcInfo] = None def start_response( - status: str, response_headers: list, exc_info: typing.Any = None - ) -> None: + status: str, + response_headers: typing.List[typing.Tuple[str, str]], + exc_info: typing.Optional[OptExcInfo] = None, + ) -> typing.Callable[[bytes], object]: nonlocal seen_status, seen_response_headers, seen_exc_info seen_status = status seen_response_headers = response_headers seen_exc_info = exc_info + return lambda _: None result = self.app(environ, start_response) @@ -122,7 +131,7 @@ def start_response( assert seen_status is not None assert seen_response_headers is not None - if seen_exc_info and self.raise_app_exceptions: + if seen_exc_info and seen_exc_info[0] and self.raise_app_exceptions: raise seen_exc_info[1] status_code = int(seen_status.split()[0]) diff --git a/httpx/_urls.py b/httpx/_urls.py index 1211bbba9a..05db165252 100644 --- a/httpx/_urls.py +++ b/httpx/_urls.py @@ -570,7 +570,7 @@ def __init__( for k, v in dict_value.items() } - def keys(self) -> typing.KeysView: + def keys(self) -> typing.KeysView[str]: """ Return all the keys in the query params. @@ -581,7 +581,7 @@ def keys(self) -> typing.KeysView: """ return self._dict.keys() - def values(self) -> typing.ValuesView: + def values(self) -> typing.ValuesView[str]: """ Return all the values in the query params. If a key occurs more than once only the first item for that key is returned. @@ -593,7 +593,7 @@ def values(self) -> typing.ValuesView: """ return {k: v[0] for k, v in self._dict.items()}.values() - def items(self) -> typing.ItemsView: + def items(self) -> typing.ItemsView[str, str]: """ Return all items in the query params. If a key occurs more than once only the first item for that key is returned. diff --git a/httpx/_utils.py b/httpx/_utils.py index ecce4f4175..6c9bcb7217 100644 --- a/httpx/_utils.py +++ b/httpx/_utils.py @@ -508,7 +508,7 @@ def matches(self, other: "URL") -> bool: return True @property - def priority(self) -> tuple: + def priority(self) -> typing.Tuple[int, int, int]: """ The priority allows URLPattern instances to be sortable, so that we can match from most specific to least specific. diff --git a/setup.cfg b/setup.cfg index 3b085d087c..d2ead38ce1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,7 @@ disallow_untyped_defs = True ignore_missing_imports = True no_implicit_optional = True show_error_codes = True +disallow_any_generics = True [mypy-tests.*] disallow_untyped_defs = False diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 8caaeb5d49..f9adaadb94 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -428,7 +428,7 @@ async def test_digest_auth( assert response.status_code == 200 assert len(response.history) == 1 - authorization = typing.cast(dict, response.json())["auth"] + authorization = typing.cast(typing.Dict[str, typing.Any], response.json())["auth"] scheme, _, fields = authorization.partition(" ") assert scheme == "Digest" @@ -459,7 +459,7 @@ async def test_digest_auth_no_specified_qop() -> None: assert response.status_code == 200 assert len(response.history) == 1 - authorization = typing.cast(dict, response.json())["auth"] + authorization = typing.cast(typing.Dict[str, typing.Any], response.json())["auth"] scheme, _, fields = authorization.partition(" ") assert scheme == "Digest" From 80c7986ec2586d4be79c901b1916754bce4c1d77 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Mon, 21 Nov 2022 19:13:21 +0000 Subject: [PATCH 2/7] Fix MutableMapping --- httpx/_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index 4a9e4a4960..5fb573cba6 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -3,7 +3,7 @@ import json as jsonlib import typing import urllib.request -from collections.abc import Mapping, MutableMapping +from collections.abc import Mapping from http.cookiejar import Cookie, CookieJar from ._content import ByteStream, UnattachedStream, encode_request, encode_response @@ -1002,7 +1002,7 @@ async def aclose(self) -> None: await self.stream.aclose() -class Cookies(MutableMapping[str, str]): +class Cookies(typing.MutableMapping[str, str]): """ HTTP Cookies, as a mutable mapping. """ From ab3d56c295b4eaedaba2585cbb2e6fe100f83950 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 22 Nov 2022 09:39:28 +0000 Subject: [PATCH 3/7] Quote typeshed types --- httpx/_transports/asgi.py | 4 ++-- httpx/_transports/wsgi.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py index ef8250f7af..695f8389b0 100644 --- a/httpx/_transports/asgi.py +++ b/httpx/_transports/asgi.py @@ -122,7 +122,7 @@ async def handle_async_request( # ASGI callables. - async def receive() -> Message: + async def receive() -> "Message": nonlocal request_complete if request_complete: @@ -136,7 +136,7 @@ async def receive() -> Message: return {"type": "http.request", "body": b"", "more_body": False} return {"type": "http.request", "body": body, "more_body": True} - async def send(message: Message) -> None: + async def send(message: "Message") -> None: nonlocal status_code, response_headers, response_started if message["type"] == "http.response.start": diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py index bf6978e330..9a5a8808db 100644 --- a/httpx/_transports/wsgi.py +++ b/httpx/_transports/wsgi.py @@ -112,12 +112,12 @@ def handle_request(self, request: Request) -> Response: seen_status = None seen_response_headers = None - seen_exc_info: typing.Optional[OptExcInfo] = None + seen_exc_info: typing.Optional["OptExcInfo"] = None def start_response( status: str, response_headers: typing.List[typing.Tuple[str, str]], - exc_info: typing.Optional[OptExcInfo] = None, + exc_info: typing.Optional["OptExcInfo"] = None, ) -> typing.Callable[[bytes], object]: nonlocal seen_status, seen_response_headers, seen_exc_info seen_status = status From b99fdf2903ef58528f5431d804c2c2e4be1ff2d7 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 22 Nov 2022 09:58:41 +0000 Subject: [PATCH 4/7] Fix coverage --- httpx/_transports/wsgi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/httpx/_transports/wsgi.py b/httpx/_transports/wsgi.py index 9a5a8808db..7b911e08c2 100644 --- a/httpx/_transports/wsgi.py +++ b/httpx/_transports/wsgi.py @@ -8,13 +8,13 @@ from .base import BaseTransport if typing.TYPE_CHECKING: - from _typeshed import OptExcInfo - from _typeshed.wsgi import WSGIApplication + from _typeshed import OptExcInfo # pragma: no cover + from _typeshed.wsgi import WSGIApplication # pragma: no cover - _T = typing.TypeVar("_T") +_T = typing.TypeVar("_T") -def _skip_leading_empty_chunks(body: typing.Iterable["_T"]) -> typing.Iterable["_T"]: +def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]: body = iter(body) for chunk in body: if chunk: From a2fb650645c4aa5cd73e2462bfe3fc5f985f6dc5 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Nov 2022 11:50:59 +0000 Subject: [PATCH 5/7] Resolve conflict --- httpx/_compat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/httpx/_compat.py b/httpx/_compat.py index f8f7c5fb64..6d96ad27a1 100644 --- a/httpx/_compat.py +++ b/httpx/_compat.py @@ -2,7 +2,6 @@ The _compat module is used for code which requires branching between different Python environments. It is excluded from the code coverage checks. """ - import ssl import sys From b7e8e83fbcb897872592e7534d39408400d62a2e Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Nov 2022 11:55:34 +0000 Subject: [PATCH 6/7] Don't import private type --- httpx/_auth.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/httpx/_auth.py b/httpx/_auth.py index 0f54be9b40..fb120116e0 100644 --- a/httpx/_auth.py +++ b/httpx/_auth.py @@ -11,7 +11,11 @@ from ._utils import to_bytes, to_str, unquote if typing.TYPE_CHECKING: # pragma: no cover - from hashlib import _Hash + from typing import Protocol + + class SupportsHexDigest(Protocol): + def hexdigest(self) -> str: + ... class Auth: @@ -142,7 +146,9 @@ def _build_auth_header( class DigestAuth(Auth): - _ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable[[bytes], "_Hash"]] = { + _ALGORITHM_TO_HASH_FUNCTION: typing.Dict[ + str, typing.Callable[[bytes], "SupportsHexDigest"] + ] = { "MD5": hashlib.md5, "MD5-SESS": hashlib.md5, "SHA": hashlib.sha1, From cafe62dbad59d4cdc856c9cf723f5429dcf477bb Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 29 Nov 2022 12:02:21 +0000 Subject: [PATCH 7/7] Resolve conflicts --- httpx/_transports/mock.py | 1 - 1 file changed, 1 deletion(-) diff --git a/httpx/_transports/mock.py b/httpx/_transports/mock.py index a243dff3ba..1434166dd5 100644 --- a/httpx/_transports/mock.py +++ b/httpx/_transports/mock.py @@ -1,6 +1,5 @@ import asyncio import typing -from asyncio import iscoroutine from .._models import Request, Response from .base import AsyncBaseTransport, BaseTransport