Skip to content

Commit

Permalink
made dependencies on certifi and httpcore only load when required (#3377
Browse files Browse the repository at this point in the history
)

Co-authored-by: Tom Christie <tom@tomchristie.com>
  • Loading branch information
joemarshall and tomchristie authored Oct 29, 2024
1 parent eeb5e3c commit e9cabc8
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 26 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion httpx/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
__version__ = "0.28.0"
__version__ = "0.27.2"
4 changes: 2 additions & 2 deletions httpx/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import typing
import warnings

import certifi

from ._models import Headers
from ._types import HeaderTypes, TimeoutTypes
from ._urls import URL
Expand Down Expand Up @@ -77,6 +75,8 @@ def __init__(
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`)
Expand Down
4 changes: 3 additions & 1 deletion httpx/_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import typing

import click
import httpcore
import pygments.lexers
import pygments.util
import rich.console
Expand All @@ -21,6 +20,9 @@
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()
Expand Down
56 changes: 36 additions & 20 deletions httpx/_transports/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@
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 (
Expand Down Expand Up @@ -66,9 +68,35 @@

__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:
Expand All @@ -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
Expand Down Expand Up @@ -138,6 +148,8 @@ def __init__(
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...
Expand Down Expand Up @@ -225,6 +237,7 @@ def handle_request(
request: Request,
) -> Response:
assert isinstance(request.stream, SyncByteStream)
import httpcore

req = httpcore.Request(
method=request.method,
Expand Down Expand Up @@ -284,6 +297,8 @@ def __init__(
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...
Expand Down Expand Up @@ -371,6 +386,7 @@ async def handle_async_request(
request: Request,
) -> Response:
assert isinstance(request.stream, AsyncByteStream)
import httpcore

req = httpcore.Request(
method=request.method,
Expand Down
15 changes: 15 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit e9cabc8

Please sign in to comment.