From 3061170eb73dccd44ca4bbcdecb3f243f95f580b Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 15:25:36 +0000 Subject: [PATCH 01/20] Enable Mypy strict in config file --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index a3d719d5c..0f23b98e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,9 +4,7 @@ max-line-length = 120 exclude = httpcore/_sync,tests/_sync [mypy] -disallow_untyped_defs = True -ignore_missing_imports = True -no_implicit_optional = True +strict = True show_error_codes = True [mypy-tests.*] From 567f3be5ce43bade7d2d6be7e35ed5633e489261 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 15:31:28 +0000 Subject: [PATCH 02/20] Ignore third party libs that don't have types --- setup.cfg | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/setup.cfg b/setup.cfg index 0f23b98e8..ae78ebb08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,21 @@ exclude = httpcore/_sync,tests/_sync strict = True show_error_codes = True +[mypy-async_generator.*] +ignore_missing_imports = True + +[mypy-h2.*] +ignore_missing_imports = True + +[mypy-h11.*] +ignore_missing_imports = True + +[mypy-hpack.*] +ignore_missing_imports = True + +[mypy-trio.*] +ignore_missing_imports = True + [mypy-tests.*] disallow_untyped_defs = False check_untyped_defs = True From a3496c52bc1d816e842a626a38b3e93ec119fe07 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 15:37:02 +0000 Subject: [PATCH 03/20] Remove unused "type: ignore" comments --- httpcore/_compat.py | 4 ++-- httpcore/backends/trio.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/httpcore/_compat.py b/httpcore/_compat.py index aa4f5bd3f..f97b45071 100644 --- a/httpcore/_compat.py +++ b/httpcore/_compat.py @@ -1,6 +1,6 @@ # `contextlib.asynccontextmanager` exists from Python 3.7 onwards. # For 3.6 we require the `async_generator` package for a backported version. try: - from contextlib import asynccontextmanager # type: ignore + from contextlib import asynccontextmanager except ImportError: - from async_generator import asynccontextmanager # type: ignore # noqa + from async_generator import asynccontextmanager # type: ignore[no-redef] # noqa diff --git a/httpcore/backends/trio.py b/httpcore/backends/trio.py index 1e1f47da6..8747dc6c8 100644 --- a/httpcore/backends/trio.py +++ b/httpcore/backends/trio.py @@ -75,7 +75,7 @@ async def start_tls( def get_extra_info(self, info: str) -> typing.Any: if info == "ssl_object" and isinstance(self._stream, trio.SSLStream): - return self._stream._ssl_object # type: ignore + return self._stream._ssl_object if info == "client_addr": return self._get_socket_stream().socket.getsockname() if info == "server_addr": From c80846a5cbdc4663990e1944232c7a39694a8ca2 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 15:38:46 +0000 Subject: [PATCH 04/20] Explicitly export asynccontextmanager --- httpcore/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpcore/_compat.py b/httpcore/_compat.py index f97b45071..b117b17dd 100644 --- a/httpcore/_compat.py +++ b/httpcore/_compat.py @@ -1,6 +1,6 @@ # `contextlib.asynccontextmanager` exists from Python 3.7 onwards. # For 3.6 we require the `async_generator` package for a backported version. try: - from contextlib import asynccontextmanager + from contextlib import asynccontextmanager as asynccontextmanager except ImportError: from async_generator import asynccontextmanager # type: ignore[no-redef] # noqa From 176099135ca8d20ab6cb21bc2472b206559a4ab0 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 16:15:53 +0000 Subject: [PATCH 05/20] Use stricter header types everywhere --- httpcore/_api.py | 5 +++-- httpcore/_async/http_proxy.py | 6 ++---- httpcore/_async/interfaces.py | 5 +++-- httpcore/_models.py | 10 ++++------ httpcore/_sync/http_proxy.py | 6 ++---- httpcore/_sync/interfaces.py | 5 +++-- httpcore/_types.py | 4 ++++ 7 files changed, 21 insertions(+), 20 deletions(-) create mode 100644 httpcore/_types.py diff --git a/httpcore/_api.py b/httpcore/_api.py index 859a189c1..c6f38b3df 100644 --- a/httpcore/_api.py +++ b/httpcore/_api.py @@ -3,13 +3,14 @@ from ._models import URL, Response from ._sync.connection_pool import ConnectionPool +from ._types import HeadersAsMapping, HeadersAsSequence def request( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, extensions: Optional[dict] = None, ) -> Response: @@ -50,7 +51,7 @@ def stream( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, extensions: Optional[dict] = None, ) -> Iterator[Response]: diff --git a/httpcore/_async/http_proxy.py b/httpcore/_async/http_proxy.py index 48d37cd36..bf0fcc7a5 100644 --- a/httpcore/_async/http_proxy.py +++ b/httpcore/_async/http_proxy.py @@ -1,6 +1,6 @@ import ssl from base64 import b64encode -from typing import List, Mapping, Optional, Sequence, Tuple, Union +from typing import List, Optional, Sequence, Tuple, Union from .._exceptions import ProxyError from .._models import ( @@ -15,15 +15,13 @@ from .._ssl import default_ssl_context from .._synchronization import AsyncLock from .._trace import Trace +from .._types import HeadersAsMapping, HeadersAsSequence from ..backends.base import AsyncNetworkBackend from .connection import AsyncHTTPConnection from .connection_pool import AsyncConnectionPool from .http11 import AsyncHTTP11Connection from .interfaces import AsyncConnectionInterface -HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] -HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] - def merge_headers( default_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, diff --git a/httpcore/_async/interfaces.py b/httpcore/_async/interfaces.py index 95165724e..1c35846ee 100644 --- a/httpcore/_async/interfaces.py +++ b/httpcore/_async/interfaces.py @@ -11,6 +11,7 @@ enforce_url, include_request_headers, ) +from .._types import HeadersAsMapping, HeadersAsSequence class AsyncRequestInterface: @@ -19,7 +20,7 @@ async def request( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, AsyncIterator[bytes], None] = None, extensions: Optional[dict] = None, ) -> Response: @@ -51,7 +52,7 @@ async def stream( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, AsyncIterator[bytes], None] = None, extensions: Optional[dict] = None, ) -> AsyncIterator[Response]: diff --git a/httpcore/_models.py b/httpcore/_models.py index 170011c68..ca52085af 100644 --- a/httpcore/_models.py +++ b/httpcore/_models.py @@ -13,11 +13,9 @@ ) from urllib.parse import urlparse -# Functions for typechecking... - +from ._types import HeadersAsMapping, HeadersAsSequence -HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] -HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] +# Functions for typechecking... def enforce_bytes(value: Union[bytes, str], *, name: str) -> bytes: @@ -318,7 +316,7 @@ def __init__( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, extensions: Optional[dict] = None, ) -> None: @@ -356,7 +354,7 @@ def __init__( self, status: int, *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, extensions: Optional[dict] = None, ) -> None: diff --git a/httpcore/_sync/http_proxy.py b/httpcore/_sync/http_proxy.py index 3dadf9af7..af9ccc1ff 100644 --- a/httpcore/_sync/http_proxy.py +++ b/httpcore/_sync/http_proxy.py @@ -1,6 +1,6 @@ import ssl from base64 import b64encode -from typing import List, Mapping, Optional, Sequence, Tuple, Union +from typing import List, Optional, Sequence, Tuple, Union from .._exceptions import ProxyError from .._models import ( @@ -15,15 +15,13 @@ from .._ssl import default_ssl_context from .._synchronization import Lock from .._trace import Trace +from .._types import HeadersAsMapping, HeadersAsSequence from ..backends.base import NetworkBackend from .connection import HTTPConnection from .connection_pool import ConnectionPool from .http11 import HTTP11Connection from .interfaces import ConnectionInterface -HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] -HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] - def merge_headers( default_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None, diff --git a/httpcore/_sync/interfaces.py b/httpcore/_sync/interfaces.py index df2f2c198..917ea5f87 100644 --- a/httpcore/_sync/interfaces.py +++ b/httpcore/_sync/interfaces.py @@ -11,6 +11,7 @@ enforce_url, include_request_headers, ) +from .._types import HeadersAsMapping, HeadersAsSequence class RequestInterface: @@ -19,7 +20,7 @@ def request( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, extensions: Optional[dict] = None, ) -> Response: @@ -51,7 +52,7 @@ def stream( method: Union[bytes, str], url: Union[URL, bytes, str], *, - headers: Union[dict, list, None] = None, + headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, extensions: Optional[dict] = None, ) -> Iterator[Response]: diff --git a/httpcore/_types.py b/httpcore/_types.py new file mode 100644 index 000000000..1a3834f48 --- /dev/null +++ b/httpcore/_types.py @@ -0,0 +1,4 @@ +from typing import Mapping, Sequence, Tuple, Union + +HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] +HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] From bf61114aaa65bb1559be84833b0647505cfce972 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 16:34:06 +0000 Subject: [PATCH 06/20] Type extensions dict more strictly --- httpcore/_api.py | 6 +++--- httpcore/_async/interfaces.py | 6 +++--- httpcore/_models.py | 7 ++++--- httpcore/_sync/interfaces.py | 6 +++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/httpcore/_api.py b/httpcore/_api.py index c6f38b3df..cdffd727b 100644 --- a/httpcore/_api.py +++ b/httpcore/_api.py @@ -1,5 +1,5 @@ from contextlib import contextmanager -from typing import Iterator, Optional, Union +from typing import Any, Dict, Iterator, Optional, Union from ._models import URL, Response from ._sync.connection_pool import ConnectionPool @@ -12,7 +12,7 @@ def request( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> Response: """ Sends an HTTP request, returning the response. @@ -53,7 +53,7 @@ def stream( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> Iterator[Response]: """ Sends an HTTP request, returning the response within a content manager. diff --git a/httpcore/_async/interfaces.py b/httpcore/_async/interfaces.py index 1c35846ee..dfd2f4acf 100644 --- a/httpcore/_async/interfaces.py +++ b/httpcore/_async/interfaces.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator, Optional, Union +from typing import Any, AsyncIterator, Dict, Optional, Union from .._compat import asynccontextmanager from .._models import ( @@ -22,7 +22,7 @@ async def request( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, AsyncIterator[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> Response: # Strict type checking on our parameters. method = enforce_bytes(method, name="method") @@ -54,7 +54,7 @@ async def stream( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, AsyncIterator[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> AsyncIterator[Response]: # Strict type checking on our parameters. method = enforce_bytes(method, name="method") diff --git a/httpcore/_models.py b/httpcore/_models.py index ca52085af..9572426f0 100644 --- a/httpcore/_models.py +++ b/httpcore/_models.py @@ -2,6 +2,7 @@ Any, AsyncIterable, AsyncIterator, + Dict, Iterable, Iterator, List, @@ -318,7 +319,7 @@ def __init__( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> None: """ Parameters: @@ -356,7 +357,7 @@ def __init__( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> None: """ Parameters: @@ -374,7 +375,7 @@ def __init__( self.stream: Union[Iterable[bytes], AsyncIterable[bytes]] = enforce_stream( content, name="content" ) - self.extensions: dict = {} if extensions is None else extensions + self.extensions: Dict[str, Any] = {} if extensions is None else extensions self._stream_consumed = False diff --git a/httpcore/_sync/interfaces.py b/httpcore/_sync/interfaces.py index 917ea5f87..ec86e5abd 100644 --- a/httpcore/_sync/interfaces.py +++ b/httpcore/_sync/interfaces.py @@ -1,4 +1,4 @@ -from typing import Iterator, Optional, Union +from typing import Any, Iterator, Dict, Optional, Union from contextlib import contextmanager from .._models import ( @@ -22,7 +22,7 @@ def request( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> Response: # Strict type checking on our parameters. method = enforce_bytes(method, name="method") @@ -54,7 +54,7 @@ def stream( *, headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None, content: Union[bytes, Iterator[bytes], None] = None, - extensions: Optional[dict] = None, + extensions: Optional[Dict[str, Any]] = None, ) -> Iterator[Response]: # Strict type checking on our parameters. method = enforce_bytes(method, name="method") From 5036692fe97d31a803f241f16fe5a1cf614544d5 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 16:45:50 +0000 Subject: [PATCH 07/20] Add type parameters for generic dict --- httpcore/backends/trio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/httpcore/backends/trio.py b/httpcore/backends/trio.py index 8747dc6c8..7ff3e4922 100644 --- a/httpcore/backends/trio.py +++ b/httpcore/backends/trio.py @@ -115,7 +115,9 @@ async def connect_tcp( # Trio supports 'local_address' from 0.16.1 onwards. # We only include the keyword argument if a local_address # argument has been passed. - kwargs: dict = {} if local_address is None else {"local_address": local_address} + kwargs: typing.Dict[str, str] = ( + {} if local_address is None else {"local_address": local_address} + ) with map_exceptions(exc_map): with trio.fail_after(timeout_or_inf): stream: trio.abc.Stream = await trio.open_tcp_stream( From 09cc801e3ec1b3b6aacc67d0bd0b7f8ef363159c Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 16:46:22 +0000 Subject: [PATCH 08/20] Cast return type of h2 lib function --- httpcore/_async/http2.py | 17 +++++++++++------ httpcore/_sync/http2.py | 17 +++++++++++------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/httpcore/_async/http2.py b/httpcore/_async/http2.py index 4be4789f4..b380a8501 100644 --- a/httpcore/_async/http2.py +++ b/httpcore/_async/http2.py @@ -342,8 +342,9 @@ async def _read_incoming_data( self._connection_error = True raise exc - events = self._h2_state.receive_data(data) - + events = typing.cast( + typing.List[h2.events.Event], self._h2_state.receive_data(data) + ) return events async def _write_outgoing_data(self, request: Request) -> None: @@ -381,13 +382,17 @@ async def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int WindowUpdated frames have increased the flow rate. https://tools.ietf.org/html/rfc7540#section-6.9 """ - local_flow = self._h2_state.local_flow_control_window(stream_id) - max_frame_size = self._h2_state.max_outbound_frame_size + local_flow = typing.cast( + int, self._h2_state.local_flow_control_window(stream_id) + ) + max_frame_size = typing.cast(int, self._h2_state.max_outbound_frame_size) flow = min(local_flow, max_frame_size) while flow == 0: await self._receive_events(request) - local_flow = self._h2_state.local_flow_control_window(stream_id) - max_frame_size = self._h2_state.max_outbound_frame_size + local_flow = typing.cast( + int, self._h2_state.local_flow_control_window(stream_id) + ) + max_frame_size = typing.cast(int, self._h2_state.max_outbound_frame_size) flow = min(local_flow, max_frame_size) return flow diff --git a/httpcore/_sync/http2.py b/httpcore/_sync/http2.py index 5bd2de02a..0e57c89de 100644 --- a/httpcore/_sync/http2.py +++ b/httpcore/_sync/http2.py @@ -342,8 +342,9 @@ def _read_incoming_data( self._connection_error = True raise exc - events = self._h2_state.receive_data(data) - + events = typing.cast( + typing.List[h2.events.Event], self._h2_state.receive_data(data) + ) return events def _write_outgoing_data(self, request: Request) -> None: @@ -381,13 +382,17 @@ def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int: WindowUpdated frames have increased the flow rate. https://tools.ietf.org/html/rfc7540#section-6.9 """ - local_flow = self._h2_state.local_flow_control_window(stream_id) - max_frame_size = self._h2_state.max_outbound_frame_size + local_flow = typing.cast( + int, self._h2_state.local_flow_control_window(stream_id) + ) + max_frame_size = typing.cast(int, self._h2_state.max_outbound_frame_size) flow = min(local_flow, max_frame_size) while flow == 0: self._receive_events(request) - local_flow = self._h2_state.local_flow_control_window(stream_id) - max_frame_size = self._h2_state.max_outbound_frame_size + local_flow = typing.cast( + int, self._h2_state.local_flow_control_window(stream_id) + ) + max_frame_size = typing.cast(int, self._h2_state.max_outbound_frame_size) flow = min(local_flow, max_frame_size) return flow From e32063e642eb68189a2a91ed738b386feda5c881 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 16:56:48 +0000 Subject: [PATCH 09/20] Cast trio return value --- httpcore/backends/trio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/httpcore/backends/trio.py b/httpcore/backends/trio.py index 7ff3e4922..05db0908a 100644 --- a/httpcore/backends/trio.py +++ b/httpcore/backends/trio.py @@ -26,7 +26,9 @@ async def read( exc_map = {trio.TooSlowError: ReadTimeout, trio.BrokenResourceError: ReadError} with map_exceptions(exc_map): with trio.fail_after(timeout_or_inf): - return await self._stream.receive_some(max_bytes=max_bytes) + return typing.cast( + bytes, await self._stream.receive_some(max_bytes=max_bytes) + ) async def write( self, buffer: bytes, timeout: typing.Optional[float] = None From dc65eafa7e10e9f2073a76a737cfbbf06ce6cd0e Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 16:58:13 +0000 Subject: [PATCH 10/20] Remove unecessary type hint --- httpcore/_synchronization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpcore/_synchronization.py b/httpcore/_synchronization.py index 3885ca7fb..cfd6b5c7c 100644 --- a/httpcore/_synchronization.py +++ b/httpcore/_synchronization.py @@ -32,7 +32,7 @@ def set(self) -> None: self._event.set() async def wait(self, timeout: Optional[float] = None) -> None: - exc_map: dict = {TimeoutError: PoolTimeout} + exc_map = {TimeoutError: PoolTimeout} with map_exceptions(exc_map): with anyio.fail_after(timeout): await self._event.wait() From 3fe4391510b54a640427fe1d93daa91a7659e972 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 17:00:21 +0000 Subject: [PATCH 11/20] Add type params to generic dict type --- httpcore/_trace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/httpcore/_trace.py b/httpcore/_trace.py index c957c2cfe..63cb18fd1 100644 --- a/httpcore/_trace.py +++ b/httpcore/_trace.py @@ -1,12 +1,12 @@ from types import TracebackType -from typing import Any, Optional, Type +from typing import Any, Dict, Optional, Type from ._models import Request class Trace: def __init__( - self, name: str, request: Request, kwargs: Optional[dict] = None + self, name: str, request: Request, kwargs: Optional[Dict[str, Any]] = None ) -> None: self.name = name self.trace = request.extensions.get("trace") @@ -27,7 +27,7 @@ def __exit__( ) -> None: if self.trace is not None: if exc_value is None: - info: dict = {"return_value": self.return_value} + info: Dict[str, Any] = {"return_value": self.return_value} self.trace(f"{self.name}.complete", info) else: info = {"exception": exc_value} @@ -47,7 +47,7 @@ async def __aexit__( ) -> None: if self.trace is not None: if exc_value is None: - info: dict = {"return_value": self.return_value} + info: Dict[str, Any] = {"return_value": self.return_value} await self.trace(f"{self.name}.complete", info) else: info = {"exception": exc_value} From 3eaa12c0dcafad812ff2aba7c445a3edee324e73 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 17:07:52 +0000 Subject: [PATCH 12/20] Add types untyped tests --- tests/_async/test_connection.py | 4 ++-- tests/_async/test_connection_pool.py | 4 ++-- tests/_sync/test_connection.py | 4 ++-- tests/_sync/test_connection_pool.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/_async/test_connection.py b/tests/_async/test_connection.py index 7405c430e..16cacbab7 100644 --- a/tests/_async/test_connection.py +++ b/tests/_async/test_connection.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional import hpack import hyperframe.frame @@ -124,7 +124,7 @@ async def test_request_to_incorrect_origin(): class NeedsRetryBackend(AsyncMockBackend): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: self._retry = 2 super().__init__(*args, **kwargs) diff --git a/tests/_async/test_connection_pool.py b/tests/_async/test_connection_pool.py index 191912f44..581c3b857 100644 --- a/tests/_async/test_connection_pool.py +++ b/tests/_async/test_connection_pool.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, NoReturn, Optional import pytest import trio as concurrency @@ -206,7 +206,7 @@ async def connect_tcp( port: int, timeout: Optional[float] = None, local_address: Optional[str] = None, - ): + ) -> NoReturn: raise ConnectError("Could not connect") network_backend = FailedConnectBackend([]) diff --git a/tests/_sync/test_connection.py b/tests/_sync/test_connection.py index 7c5a0c164..63b8732c4 100644 --- a/tests/_sync/test_connection.py +++ b/tests/_sync/test_connection.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional import hpack import hyperframe.frame @@ -124,7 +124,7 @@ def test_request_to_incorrect_origin(): class NeedsRetryBackend(MockBackend): - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: self._retry = 2 super().__init__(*args, **kwargs) diff --git a/tests/_sync/test_connection_pool.py b/tests/_sync/test_connection_pool.py index 4ba3f6a37..1e2a6f477 100644 --- a/tests/_sync/test_connection_pool.py +++ b/tests/_sync/test_connection_pool.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, NoReturn, Optional import pytest from tests import concurrency @@ -206,7 +206,7 @@ def connect_tcp( port: int, timeout: Optional[float] = None, local_address: Optional[str] = None, - ): + ) -> NoReturn: raise ConnectError("Could not connect") network_backend = FailedConnectBackend([]) From 1dfbefeb39ef3ba88967ee8863836063b6fa4f69 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 17:11:55 +0000 Subject: [PATCH 13/20] Type testing Nursery class --- tests/concurrency.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/concurrency.py b/tests/concurrency.py index b49dec6dd..e8431124a 100644 --- a/tests/concurrency.py +++ b/tests/concurrency.py @@ -10,7 +10,7 @@ """ import threading from types import TracebackType -from typing import List, Optional, Type +from typing import Any, Callable, List, Optional, Type class Nursery: @@ -31,7 +31,7 @@ def __exit__( for thread in self._threads: thread.join() - def start_soon(self, func, *args): + def start_soon(self, func: Callable[..., Any], *args: Any) -> None: thread = threading.Thread(target=func, args=args) self._threads.append(thread) From 9846be29123d1af8027f70cc4e7c928e3cce893e Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 4 Mar 2022 17:20:58 +0000 Subject: [PATCH 14/20] Ignore exception types --- httpcore/_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpcore/_exceptions.py b/httpcore/_exceptions.py index f28dcc256..73eaaccd9 100644 --- a/httpcore/_exceptions.py +++ b/httpcore/_exceptions.py @@ -3,7 +3,7 @@ @contextlib.contextmanager -def map_exceptions(map: Dict[Type, Type]) -> Iterator[None]: +def map_exceptions(map: Dict[Type, Type]) -> Iterator[None]: # type: ignore[type-arg] try: yield except Exception as exc: # noqa: PIE786 From d5b65266a777573a200d4d6cd7542b3e90c26557 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 6 Mar 2022 17:10:15 +0000 Subject: [PATCH 15/20] Check python version rather than replying on ImportError --- httpcore/_compat.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/httpcore/_compat.py b/httpcore/_compat.py index b117b17dd..42136221b 100644 --- a/httpcore/_compat.py +++ b/httpcore/_compat.py @@ -1,6 +1,8 @@ # `contextlib.asynccontextmanager` exists from Python 3.7 onwards. # For 3.6 we require the `async_generator` package for a backported version. -try: +import sys + +if sys.version_info >= (3, 7): from contextlib import asynccontextmanager as asynccontextmanager -except ImportError: - from async_generator import asynccontextmanager # type: ignore[no-redef] # noqa +else: + from async_generator import asynccontextmanager # noqa: F401 From c0178f00fe8a5e066d2aa3595bc2ee164398fcce Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 6 Mar 2022 17:13:10 +0000 Subject: [PATCH 16/20] Explicity re-export asynccontextmanager --- httpcore/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpcore/_compat.py b/httpcore/_compat.py index 42136221b..a3c0f81d9 100644 --- a/httpcore/_compat.py +++ b/httpcore/_compat.py @@ -5,4 +5,4 @@ if sys.version_info >= (3, 7): from contextlib import asynccontextmanager as asynccontextmanager else: - from async_generator import asynccontextmanager # noqa: F401 + from async_generator import asynccontextmanager as asynccontextmanager # noqa: F401 From 3824d78fb83954cf7dc287d26b7318b1690f5d03 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 6 Mar 2022 17:29:45 +0000 Subject: [PATCH 17/20] Fix CI for Python 3.6 --- httpcore/_async/interfaces.py | 2 +- httpcore/_sync/interfaces.py | 2 +- setup.cfg | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/httpcore/_async/interfaces.py b/httpcore/_async/interfaces.py index dfd2f4acf..9694d63f2 100644 --- a/httpcore/_async/interfaces.py +++ b/httpcore/_async/interfaces.py @@ -46,7 +46,7 @@ async def request( await response.aclose() return response - @asynccontextmanager + @asynccontextmanager # type: ignore[misc] async def stream( self, method: Union[bytes, str], diff --git a/httpcore/_sync/interfaces.py b/httpcore/_sync/interfaces.py index ec86e5abd..8b0210de4 100644 --- a/httpcore/_sync/interfaces.py +++ b/httpcore/_sync/interfaces.py @@ -46,7 +46,7 @@ def request( response.close() return response - @contextmanager + @contextmanager # type: ignore[misc] def stream( self, method: Union[bytes, str], diff --git a/setup.cfg b/setup.cfg index ae78ebb08..e4a37d686 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,9 @@ exclude = httpcore/_sync,tests/_sync strict = True show_error_codes = True +[mypy-httpcore._sync.interfaces] +warn_unused_ignores = False + [mypy-async_generator.*] ignore_missing_imports = True From 0f0c6369de01f0edcf0a92a2a6e2e10a39b05209 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 6 Mar 2022 17:32:37 +0000 Subject: [PATCH 18/20] Add python_version to mypy config file --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index e4a37d686..87cbe27c0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,6 +4,7 @@ max-line-length = 120 exclude = httpcore/_sync,tests/_sync [mypy] +python_version = 3.6 strict = True show_error_codes = True From d1d941341b70ff6c4485703a8ec1e673ef53f568 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 6 Mar 2022 17:51:22 +0000 Subject: [PATCH 19/20] Properly type map_exceptions function --- httpcore/_exceptions.py | 6 ++++-- httpcore/_synchronization.py | 4 ++-- httpcore/backends/sync.py | 26 +++++++++++++++++++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/httpcore/_exceptions.py b/httpcore/_exceptions.py index 73eaaccd9..efae0d1ca 100644 --- a/httpcore/_exceptions.py +++ b/httpcore/_exceptions.py @@ -1,9 +1,11 @@ import contextlib -from typing import Dict, Iterator, Type +from typing import Iterator, Mapping, Type + +ExceptionMappingType = Mapping[Type[Exception], Type[Exception]] @contextlib.contextmanager -def map_exceptions(map: Dict[Type, Type]) -> Iterator[None]: # type: ignore[type-arg] +def map_exceptions(map: ExceptionMappingType) -> Iterator[None]: try: yield except Exception as exc: # noqa: PIE786 diff --git a/httpcore/_synchronization.py b/httpcore/_synchronization.py index cfd6b5c7c..87c8687de 100644 --- a/httpcore/_synchronization.py +++ b/httpcore/_synchronization.py @@ -4,7 +4,7 @@ import anyio -from ._exceptions import PoolTimeout, map_exceptions +from ._exceptions import ExceptionMappingType, PoolTimeout, map_exceptions class AsyncLock: @@ -32,7 +32,7 @@ def set(self) -> None: self._event.set() async def wait(self, timeout: Optional[float] = None) -> None: - exc_map = {TimeoutError: PoolTimeout} + exc_map: ExceptionMappingType = {TimeoutError: PoolTimeout} with map_exceptions(exc_map): with anyio.fail_after(timeout): await self._event.wait() diff --git a/httpcore/backends/sync.py b/httpcore/backends/sync.py index 18e1dd348..ba3df7fb3 100644 --- a/httpcore/backends/sync.py +++ b/httpcore/backends/sync.py @@ -5,6 +5,7 @@ from .._exceptions import ( ConnectError, ConnectTimeout, + ExceptionMappingType, ReadError, ReadTimeout, WriteError, @@ -20,7 +21,10 @@ def __init__(self, sock: socket.socket) -> None: self._sock = sock def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes: - exc_map = {socket.timeout: ReadTimeout, socket.error: ReadError} + exc_map: ExceptionMappingType = { + socket.timeout: ReadTimeout, + socket.error: ReadError, + } with map_exceptions(exc_map): self._sock.settimeout(timeout) return self._sock.recv(max_bytes) @@ -29,7 +33,10 @@ def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None: if not buffer: return - exc_map = {socket.timeout: WriteTimeout, socket.error: WriteError} + exc_map: ExceptionMappingType = { + socket.timeout: WriteTimeout, + socket.error: WriteError, + } with map_exceptions(exc_map): while buffer: self._sock.settimeout(timeout) @@ -45,7 +52,10 @@ def start_tls( server_hostname: typing.Optional[str] = None, timeout: typing.Optional[float] = None, ) -> NetworkStream: - exc_map = {socket.timeout: ConnectTimeout, socket.error: ConnectError} + exc_map: ExceptionMappingType = { + socket.timeout: ConnectTimeout, + socket.error: ConnectError, + } with map_exceptions(exc_map): try: self._sock.settimeout(timeout) @@ -81,7 +91,10 @@ def connect_tcp( ) -> NetworkStream: address = (host, port) source_address = None if local_address is None else (local_address, 0) - exc_map = {socket.timeout: ConnectTimeout, socket.error: ConnectError} + exc_map: ExceptionMappingType = { + socket.timeout: ConnectTimeout, + socket.error: ConnectError, + } with map_exceptions(exc_map): sock = socket.create_connection( address, timeout, source_address=source_address @@ -91,7 +104,10 @@ def connect_tcp( def connect_unix_socket( self, path: str, timeout: typing.Optional[float] = None ) -> NetworkStream: # pragma: nocover - exc_map = {socket.timeout: ConnectTimeout, socket.error: ConnectError} + exc_map: ExceptionMappingType = { + socket.timeout: ConnectTimeout, + socket.error: ConnectError, + } with map_exceptions(exc_map): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(timeout) From daafd90b4330e9d3744d90d4477c9e5bc4dc31a1 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Sun, 6 Mar 2022 18:06:41 +0000 Subject: [PATCH 20/20] Add mypy error codes to bare `type: ignore` comments --- httpcore/_async/__init__.py | 8 ++++---- httpcore/_async/connection_pool.py | 2 +- httpcore/_models.py | 4 ++-- httpcore/_ssl.py | 2 +- httpcore/_sync/__init__.py | 8 ++++---- httpcore/_sync/connection_pool.py | 2 +- httpcore/backends/sync.py | 2 +- setup.cfg | 3 +++ tests/_async/test_http_proxy.py | 2 +- tests/_sync/test_http_proxy.py | 2 +- tests/test_models.py | 8 ++++---- 11 files changed, 23 insertions(+), 20 deletions(-) diff --git a/httpcore/_async/__init__.py b/httpcore/_async/__init__.py index 88dc7f01e..c74d9d172 100644 --- a/httpcore/_async/__init__.py +++ b/httpcore/_async/__init__.py @@ -8,8 +8,8 @@ from .http2 import AsyncHTTP2Connection except ImportError: # pragma: nocover - class AsyncHTTP2Connection: # type: ignore - def __init__(self, *args, **kwargs) -> None: # type: ignore + class AsyncHTTP2Connection: # type: ignore[no-redef] + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] raise RuntimeError( "Attempted to use http2 support, but the `h2` package is not " "installed. Use 'pip install httpcore[http2]'." @@ -20,8 +20,8 @@ def __init__(self, *args, **kwargs) -> None: # type: ignore from .socks_proxy import AsyncSOCKSProxy except ImportError: # pragma: nocover - class AsyncSOCKSProxy: # type: ignore - def __init__(self, *args, **kwargs) -> None: # type: ignore + class AsyncSOCKSProxy: # type: ignore[no-redef] + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] raise RuntimeError( "Attempted to use SOCKS support, but the `socksio` package is not " "installed. Use 'pip install httpcore[socks]'." diff --git a/httpcore/_async/connection_pool.py b/httpcore/_async/connection_pool.py index 06ead3ee0..874a908dc 100644 --- a/httpcore/_async/connection_pool.py +++ b/httpcore/_async/connection_pool.py @@ -349,6 +349,6 @@ async def __aiter__(self) -> AsyncIterator[bytes]: async def aclose(self) -> None: try: if hasattr(self._stream, "aclose"): - await self._stream.aclose() # type: ignore + await self._stream.aclose() # type: ignore[attr-defined] finally: await self._pool.response_closed(self._status) diff --git a/httpcore/_models.py b/httpcore/_models.py index 9572426f0..381205577 100644 --- a/httpcore/_models.py +++ b/httpcore/_models.py @@ -431,7 +431,7 @@ def close(self) -> None: "You should use 'await response.aclose()' instead." ) if hasattr(self.stream, "close"): - self.stream.close() # type: ignore + self.stream.close() # type: ignore[attr-defined] # Async interface... @@ -470,4 +470,4 @@ async def aclose(self) -> None: "You should use 'response.close()' instead." ) if hasattr(self.stream, "aclose"): - await self.stream.aclose() # type: ignore + await self.stream.aclose() # type: ignore[attr-defined] diff --git a/httpcore/_ssl.py b/httpcore/_ssl.py index 629f002f0..c99c5a679 100644 --- a/httpcore/_ssl.py +++ b/httpcore/_ssl.py @@ -1,6 +1,6 @@ import ssl -import certifi # type: ignore +import certifi def default_ssl_context() -> ssl.SSLContext: diff --git a/httpcore/_sync/__init__.py b/httpcore/_sync/__init__.py index b476d76d9..15611a178 100644 --- a/httpcore/_sync/__init__.py +++ b/httpcore/_sync/__init__.py @@ -8,8 +8,8 @@ from .http2 import HTTP2Connection except ImportError: # pragma: nocover - class HTTP2Connection: # type: ignore - def __init__(self, *args, **kwargs) -> None: # type: ignore + class HTTP2Connection: # type: ignore[no-redef] + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] raise RuntimeError( "Attempted to use http2 support, but the `h2` package is not " "installed. Use 'pip install httpcore[http2]'." @@ -20,8 +20,8 @@ def __init__(self, *args, **kwargs) -> None: # type: ignore from .socks_proxy import SOCKSProxy except ImportError: # pragma: nocover - class SOCKSProxy: # type: ignore - def __init__(self, *args, **kwargs) -> None: # type: ignore + class SOCKSProxy: # type: ignore[no-redef] + def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] raise RuntimeError( "Attempted to use SOCKS support, but the `socksio` package is not " "installed. Use 'pip install httpcore[socks]'." diff --git a/httpcore/_sync/connection_pool.py b/httpcore/_sync/connection_pool.py index 88b291878..14b9425a7 100644 --- a/httpcore/_sync/connection_pool.py +++ b/httpcore/_sync/connection_pool.py @@ -349,6 +349,6 @@ def __iter__(self) -> Iterator[bytes]: def close(self) -> None: try: if hasattr(self._stream, "close"): - self._stream.close() # type: ignore + self._stream.close() # type: ignore[attr-defined] finally: self._pool.response_closed(self._status) diff --git a/httpcore/backends/sync.py b/httpcore/backends/sync.py index ba3df7fb3..e4a0c9b0d 100644 --- a/httpcore/backends/sync.py +++ b/httpcore/backends/sync.py @@ -69,7 +69,7 @@ def start_tls( def get_extra_info(self, info: str) -> typing.Any: if info == "ssl_object" and isinstance(self._sock, ssl.SSLSocket): - return self._sock._sslobj # type: ignore + return self._sock._sslobj # type: ignore[attr-defined] if info == "client_addr": return self._sock.getsockname() if info == "server_addr": diff --git a/setup.cfg b/setup.cfg index 87cbe27c0..74f6bdcdb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,9 @@ show_error_codes = True [mypy-httpcore._sync.interfaces] warn_unused_ignores = False +[mypy-certifi.*] +ignore_missing_imports = True + [mypy-async_generator.*] ignore_missing_imports = True diff --git a/tests/_async/test_http_proxy.py b/tests/_async/test_http_proxy.py index f4ade94cc..6a846262f 100644 --- a/tests/_async/test_http_proxy.py +++ b/tests/_async/test_http_proxy.py @@ -259,6 +259,6 @@ async def test_proxy_tunneling_with_auth(): # Dig into this private property as a cheap lazy way of # checking that the proxy header is set correctly. - assert proxy._proxy_headers == [ # type: ignore + assert proxy._proxy_headers == [ # type: ignore[attr-defined] (b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=") ] diff --git a/tests/_sync/test_http_proxy.py b/tests/_sync/test_http_proxy.py index ebdbbb8fe..cdfdc4d2d 100644 --- a/tests/_sync/test_http_proxy.py +++ b/tests/_sync/test_http_proxy.py @@ -259,6 +259,6 @@ def test_proxy_tunneling_with_auth(): # Dig into this private property as a cheap lazy way of # checking that the proxy header is set correctly. - assert proxy._proxy_headers == [ # type: ignore + assert proxy._proxy_headers == [ # type: ignore[attr-defined] (b"Proxy-Authorization", b"Basic dXNlcm5hbWU6cGFzc3dvcmQ=") ] diff --git a/tests/test_models.py b/tests/test_models.py index 056197547..e54704c14 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -25,7 +25,7 @@ def test_url_with_port(): def test_url_with_invalid_argument(): with pytest.raises(TypeError) as exc_info: - httpcore.URL(123) # type: ignore + httpcore.URL(123) # type: ignore[arg-type] assert str(exc_info.value) == "url must be bytes or str, but got int." @@ -60,19 +60,19 @@ def test_request(): def test_request_with_invalid_method(): with pytest.raises(TypeError) as exc_info: - httpcore.Request(123, "https://www.example.com/") # type: ignore + httpcore.Request(123, "https://www.example.com/") # type: ignore[arg-type] assert str(exc_info.value) == "method must be bytes or str, but got int." def test_request_with_invalid_url(): with pytest.raises(TypeError) as exc_info: - httpcore.Request("GET", 123) # type: ignore + httpcore.Request("GET", 123) # type: ignore[arg-type] assert str(exc_info.value) == "url must be a URL, bytes, or str, but got int." def test_request_with_invalid_headers(): with pytest.raises(TypeError) as exc_info: - httpcore.Request("GET", "https://www.example.com/", headers=123) # type: ignore + httpcore.Request("GET", "https://www.example.com/", headers=123) # type: ignore[arg-type] assert ( str(exc_info.value) == "headers must be a mapping or sequence of two-tuples, but got int."