Merge branch 'master' into limit-supported-codecs

This commit is contained in:
Tom Christie 2024-10-29 14:34:59 +00:00 committed by GitHub
commit 4a29723dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 93 additions and 79 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,8 @@ 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)
* Treat `socks5h` as a valid proxy scheme. (#3178)
## 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`)
@ -262,7 +262,7 @@ class Proxy:
url = URL(url)
headers = Headers(headers)
if url.scheme not in ("http", "https", "socks5"):
if url.scheme not in ("http", "https", "socks5", "socks5h"):
raise ValueError(f"Unknown scheme for proxy URL {url!r}")
if url.username or url.password:

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...
@ -177,7 +189,7 @@ class HTTPTransport(BaseTransport):
http2=http2,
socket_options=socket_options,
)
elif proxy.url.scheme == "socks5":
elif proxy.url.scheme in ("socks5", "socks5h"):
try:
import socksio # noqa
except ImportError: # pragma: no cover
@ -203,7 +215,7 @@ class HTTPTransport(BaseTransport):
)
else: # pragma: no cover
raise ValueError(
"Proxy protocol must be either 'http', 'https', or 'socks5',"
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
f" but got {proxy.url.scheme!r}."
)
@ -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...
@ -323,7 +338,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
http2=http2,
socket_options=socket_options,
)
elif proxy.url.scheme == "socks5":
elif proxy.url.scheme in ("socks5", "socks5h"):
try:
import socksio # noqa
except ImportError: # pragma: no cover
@ -349,7 +364,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
)
else: # pragma: no cover
raise ValueError(
"Proxy protocol must be either 'http', 'https', or 'socks5',"
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
" but got {proxy.url.scheme!r}."
)
@ -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

@ -12,20 +12,6 @@ from ._utils import primitive_value_to_str
__all__ = ["URL", "QueryParams"]
# To urlencode query parameters, we use the whatwg query percent-encode set
# and additionally escape U+0025 (%), U+0026 (&), U+002B (+) and U+003D (=).
# https://url.spec.whatwg.org/#percent-encoded-bytes
URLENCODE_SAFE = "".join(
[
chr(i)
for i in range(0x20, 0x7F)
if i not in (0x20, 0x22, 0x23, 0x25, 0x26, 0x2B, 0x3C, 0x3D, 0x3E)
]
)
class URL:
"""
url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink")
@ -619,7 +605,7 @@ class QueryParams(typing.Mapping[str, str]):
return sorted(self.multi_items()) == sorted(other.multi_items())
def __str__(self) -> str:
return urlencode(self.multi_items(), safe=URLENCODE_SAFE)
return urlencode(self.multi_items())
def __repr__(self) -> str:
class_name = self.__class__.__name__

View File

@ -270,29 +270,6 @@ def test_netrc_auth_credentials_do_not_exist() -> None:
assert response.json() == {"auth": None}
@pytest.mark.skipif(
sys.version_info < (3, 11),
reason="netrc files without a password are invalid with Python < 3.11",
)
def test_netrc_auth_nopassword() -> None: # pragma: no cover
"""
Python has different netrc parsing behaviours with different versions.
For Python 3.11+ a netrc file with no password is valid. In this case
we want to check that we allow the netrc auth, and simply don't provide
any credentials in the request.
"""
netrc_file = str(FIXTURES_DIR / ".netrc-nopassword")
url = "http://example.org"
app = App()
auth = httpx.NetRCAuth(netrc_file)
with httpx.Client(transport=httpx.MockTransport(app), auth=auth) as client:
response = client.get(url)
assert response.status_code == 200
assert response.json() == {"auth": None}
@pytest.mark.skipif(
sys.version_info >= (3, 11),
reason="netrc files without a password are valid from Python >= 3.11",

View File

@ -16,15 +16,16 @@ def url_to_origin(url: str) -> httpcore.URL:
def test_socks_proxy():
url = httpx.URL("http://www.example.com")
client = httpx.Client(proxy="socks5://localhost/")
transport = client._transport_for_url(url)
assert isinstance(transport, httpx.HTTPTransport)
assert isinstance(transport._pool, httpcore.SOCKSProxy)
for proxy in ("socks5://localhost/", "socks5h://localhost/"):
client = httpx.Client(proxy=proxy)
transport = client._transport_for_url(url)
assert isinstance(transport, httpx.HTTPTransport)
assert isinstance(transport._pool, httpcore.SOCKSProxy)
async_client = httpx.AsyncClient(proxy="socks5://localhost/")
async_transport = async_client._transport_for_url(url)
assert isinstance(async_transport, httpx.AsyncHTTPTransport)
assert isinstance(async_transport._pool, httpcore.AsyncSOCKSProxy)
async_client = httpx.AsyncClient(proxy=proxy)
async_transport = async_client._transport_for_url(url)
assert isinstance(async_transport, httpx.AsyncHTTPTransport)
assert isinstance(async_transport._pool, httpcore.AsyncSOCKSProxy)
PROXY_URL = "http://[::1]"

View File

@ -148,7 +148,7 @@ def test_url_query_encoding():
assert url.raw_path == b"/?a=b+c&d=e/f"
url = httpx.URL("https://www.example.com/", params={"a": "b c", "d": "e/f"})
assert url.raw_path == b"/?a=b+c&d=e/f"
assert url.raw_path == b"/?a=b+c&d=e%2Ff"
def test_url_params():
@ -309,7 +309,7 @@ def test_param_with_existing_escape_requires_encoding():
# even if they include a valid escape sequence.
# We want to match browser form behaviour here.
url = httpx.URL("http://webservice", params={"u": "http://example.com?q=foo%2Fa"})
assert str(url) == "http://webservice?u=http://example.com?q%3Dfoo%252Fa"
assert str(url) == "http://webservice?u=http%3A%2F%2Fexample.com%3Fq%3Dfoo%252Fa"
# Tests for query parameter percent encoding.

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