]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
made dependencies on certifi and httpcore only load when required (#3377)
authorJoe Marshall <joe.marshall@nottingham.ac.uk>
Tue, 29 Oct 2024 13:18:39 +0000 (13:18 +0000)
committerGitHub <noreply@github.com>
Tue, 29 Oct 2024 13:18:39 +0000 (13:18 +0000)
Co-authored-by: Tom Christie <tom@tomchristie.com>
CHANGELOG.md
httpx/__version__.py
httpx/_config.py
httpx/_main.py
httpx/_transports/default.py
tests/test_api.py
tests/test_config.py

index 53aaa6e0facd808b2b0c7f69122e6b8212709406..1d32e53dc695cff187f8a7a1fc99a00813829532 100644 (file)
@@ -4,9 +4,9 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
-## Version 0.28.0
+## [Unreleased]
 
-Version 0.28.0 introduces an `httpx.SSLContext()` class and `ssl_context` parameter.
+This release introduces an `httpx.SSLContext()` class and `ssl_context` parameter.
 
 * Added `httpx.SSLContext` class and `ssl_context` parameter. (#3022, #3335)
 * The `verify` and `cert` arguments have been deprecated and will now raise warnings. (#3022, #3335)
@@ -15,6 +15,7 @@ Version 0.28.0 introduces an `httpx.SSLContext()` class and `ssl_context` parame
 * The `URL.raw` property has now been removed.
 * Ensure JSON request bodies are compact. (#3363)
 * Review URL percent escape sets, based on WHATWG spec. (#3371, #3373)
+* Ensure `certifi` and `httpcore` are only imported if required. (#3377)
 
 ## 0.27.2 (27th August, 2024)
 
index 0a684ac3a94b07906f2dc76f4c85136297a9bfd8..5eaaddbac9900b22dc569acbc3845c9c2d92ba59 100644 (file)
@@ -1,3 +1,3 @@
 __title__ = "httpx"
 __description__ = "A next generation HTTP client, for Python 3."
-__version__ = "0.28.0"
+__version__ = "0.27.2"
index 2c9634a66677b0ac18abc48e3888103d52c1dc1b..5a1a98a0241f179b8dbc43e5d86c9f6f8fd17073 100644 (file)
@@ -6,8 +6,6 @@ import sys
 import typing
 import warnings
 
-import certifi
-
 from ._models import Headers
 from ._types import HeaderTypes, TimeoutTypes
 from ._urls import URL
@@ -77,6 +75,8 @@ class SSLContext(ssl.SSLContext):
         self,
         verify: bool = True,
     ) -> None:
+        import certifi
+
         # ssl.SSLContext sets OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION,
         # OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE and OP_SINGLE_ECDH_USE
         # by default. (from `ssl.create_default_context`)
index 41c50f741399afb360000dd38cfe81713b577218..3df37cf0ae4ec73c08826aa1f4b7314656cc9a78 100644 (file)
@@ -6,7 +6,6 @@ import sys
 import typing
 
 import click
-import httpcore
 import pygments.lexers
 import pygments.util
 import rich.console
@@ -21,6 +20,9 @@ from ._exceptions import RequestError
 from ._models import Response
 from ._status_codes import codes
 
+if typing.TYPE_CHECKING:
+    import httpcore  # pragma: no cover
+
 
 def print_help() -> None:
     console = rich.console.Console()
index a1978c5ae99cea8c744f1a20b9f54c3a6355d5d2..50ff91055ef377b6af1af0c48ae41a5387edc760 100644 (file)
@@ -27,11 +27,13 @@ client = httpx.Client(transport=transport)
 from __future__ import annotations
 
 import contextlib
-import ssl
 import typing
 from types import TracebackType
 
-import httpcore
+if typing.TYPE_CHECKING:
+    import ssl  # pragma: no cover
+
+    import httpx  # pragma: no cover
 
 from .._config import DEFAULT_LIMITS, Limits, Proxy, SSLContext, create_ssl_context
 from .._exceptions import (
@@ -66,9 +68,35 @@ SOCKET_OPTION = typing.Union[
 
 __all__ = ["AsyncHTTPTransport", "HTTPTransport"]
 
+HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {}
+
+
+def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]:
+    import httpcore
+
+    return {
+        httpcore.TimeoutException: TimeoutException,
+        httpcore.ConnectTimeout: ConnectTimeout,
+        httpcore.ReadTimeout: ReadTimeout,
+        httpcore.WriteTimeout: WriteTimeout,
+        httpcore.PoolTimeout: PoolTimeout,
+        httpcore.NetworkError: NetworkError,
+        httpcore.ConnectError: ConnectError,
+        httpcore.ReadError: ReadError,
+        httpcore.WriteError: WriteError,
+        httpcore.ProxyError: ProxyError,
+        httpcore.UnsupportedProtocol: UnsupportedProtocol,
+        httpcore.ProtocolError: ProtocolError,
+        httpcore.LocalProtocolError: LocalProtocolError,
+        httpcore.RemoteProtocolError: RemoteProtocolError,
+    }
+
 
 @contextlib.contextmanager
 def map_httpcore_exceptions() -> typing.Iterator[None]:
+    global HTTPCORE_EXC_MAP
+    if len(HTTPCORE_EXC_MAP) == 0:
+        HTTPCORE_EXC_MAP = _load_httpcore_exceptions()
     try:
         yield
     except Exception as exc:
@@ -90,24 +118,6 @@ def map_httpcore_exceptions() -> typing.Iterator[None]:
         raise mapped_exc(message) from exc
 
 
-HTTPCORE_EXC_MAP = {
-    httpcore.TimeoutException: TimeoutException,
-    httpcore.ConnectTimeout: ConnectTimeout,
-    httpcore.ReadTimeout: ReadTimeout,
-    httpcore.WriteTimeout: WriteTimeout,
-    httpcore.PoolTimeout: PoolTimeout,
-    httpcore.NetworkError: NetworkError,
-    httpcore.ConnectError: ConnectError,
-    httpcore.ReadError: ReadError,
-    httpcore.WriteError: WriteError,
-    httpcore.ProxyError: ProxyError,
-    httpcore.UnsupportedProtocol: UnsupportedProtocol,
-    httpcore.ProtocolError: ProtocolError,
-    httpcore.LocalProtocolError: LocalProtocolError,
-    httpcore.RemoteProtocolError: RemoteProtocolError,
-}
-
-
 class ResponseStream(SyncByteStream):
     def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None:
         self._httpcore_stream = httpcore_stream
@@ -138,6 +148,8 @@ class HTTPTransport(BaseTransport):
         verify: typing.Any = None,
         cert: typing.Any = None,
     ) -> None:
+        import httpcore
+
         proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
         if verify is not None or cert is not None:  # pragma: nocover
             # Deprecated...
@@ -225,6 +237,7 @@ class HTTPTransport(BaseTransport):
         request: Request,
     ) -> Response:
         assert isinstance(request.stream, SyncByteStream)
+        import httpcore
 
         req = httpcore.Request(
             method=request.method,
@@ -284,6 +297,8 @@ class AsyncHTTPTransport(AsyncBaseTransport):
         verify: typing.Any = None,
         cert: typing.Any = None,
     ) -> None:
+        import httpcore
+
         proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
         if verify is not None or cert is not None:  # pragma: nocover
             # Deprecated...
@@ -371,6 +386,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
         request: Request,
     ) -> Response:
         assert isinstance(request.stream, AsyncByteStream)
+        import httpcore
 
         req = httpcore.Request(
             method=request.method,
index fe8083fc40d01f8158d1f961d66e6467adf1e9fb..225f384ede71ea938979ead9fd474cf24b8eb6fc 100644 (file)
@@ -85,3 +85,18 @@ def test_stream(server):
 def test_get_invalid_url():
     with pytest.raises(httpx.UnsupportedProtocol):
         httpx.get("invalid://example.org")
+
+
+# check that httpcore isn't imported until we do a request
+def test_httpcore_lazy_loading(server):
+    import sys
+
+    # unload our module if it is already loaded
+    if "httpx" in sys.modules:
+        del sys.modules["httpx"]
+        del sys.modules["httpcore"]
+    import httpx
+
+    assert "httpcore" not in sys.modules
+    _response = httpx.get(server.url)
+    assert "httpcore" in sys.modules
index 9f86f83936359a7d3822cc021850cd2f420f64d5..5d8748d169d3d9d082cfa015823347ee92a7bc65 100644 (file)
@@ -188,3 +188,18 @@ def test_proxy_with_auth_from_url():
 def test_invalid_proxy_scheme():
     with pytest.raises(ValueError):
         httpx.Proxy("invalid://example.com")
+
+
+def test_certifi_lazy_loading():
+    global httpx, certifi
+    import sys
+
+    del sys.modules["httpx"]
+    del sys.modules["certifi"]
+    del httpx
+    del certifi
+    import httpx
+
+    assert "certifi" not in sys.modules
+    _context = httpx.SSLContext()
+    assert "certifi" in sys.modules