made dependencies on certifi and httpcore only load when required (#3377)

Co-authored-by: Tom Christie <tom@tomchristie.com>
This commit is contained in:
Joe Marshall 2024-10-29 13:18:39 +00:00 committed by GitHub
parent eeb5e3c2a3
commit e9cabc8e1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 75 additions and 26 deletions

View 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)

View File

@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
__version__ = "0.28.0"
__version__ = "0.27.2"

View 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`)

View 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()

View 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,

View 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

View 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