From 189fc4bcbe5f314128775dec66a616ac9a31ad48 Mon Sep 17 00:00:00 2001
From: Bob Conan
Date: Wed, 20 Nov 2024 06:27:29 -0600
Subject: [PATCH 1/9] Update CHANGELOG.md, fix typo(s) (#3406)
---
CHANGELOG.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7ad03c0..3c16d667 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -617,7 +617,7 @@ See pull requests #1057, #1058.
* Added dedicated exception class `httpx.HTTPStatusError` for `.raise_for_status()` exceptions. (Pull #1072)
* Added `httpx.create_ssl_context()` helper function. (Pull #996)
-* Support for proxy exlcusions like `proxies={"https://www.example.com": None}`. (Pull #1099)
+* Support for proxy exclusions like `proxies={"https://www.example.com": None}`. (Pull #1099)
* Support `QueryParams(None)` and `client.params = None`. (Pull #1060)
### Changed
@@ -845,7 +845,7 @@ We believe the API is now pretty much stable, and are aiming for a 1.0 release s
### Fixed
-- Fix issue with concurrent connection acquiry. (Pull #700)
+- Fix issue with concurrent connection acquisition. (Pull #700)
- Fix write error on closing HTTP/2 connections. (Pull #699)
## 0.10.0 (December 29th, 2019)
@@ -1094,7 +1094,7 @@ importing modules within the package.
## 0.6.7 (July 8, 2019)
-- Check for connection aliveness on re-acquiry (Pull #111)
+- Check for connection aliveness on re-acquisition (Pull #111)
## 0.6.6 (July 3, 2019)
From 47f4a96ffaaaa07dca1614409549b5d7a6e7af49 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 22 Nov 2024 11:42:51 +0000
Subject: [PATCH 2/9] Handle empty zstd responses (#3412)
---
CHANGELOG.md | 2 +-
README.md | 4 +---
httpx/__version__.py | 2 +-
httpx/_decoders.py | 4 ++++
tests/test_decoders.py | 19 +++++++++++++++++++
5 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c16d667..4e2afe2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ 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/).
-## [Unreleased]
+## 0.28.0 (...)
The 0.28 release includes a limited set of backwards incompatible changes.
diff --git a/README.md b/README.md
index d5d21487..23992d9c 100644
--- a/README.md
+++ b/README.md
@@ -13,9 +13,7 @@
-HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated
-command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync
-and async APIs**.
+HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
---
diff --git a/httpx/__version__.py b/httpx/__version__.py
index 5eaaddba..0a684ac3 100644
--- a/httpx/__version__.py
+++ b/httpx/__version__.py
@@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
-__version__ = "0.27.2"
+__version__ = "0.28.0"
diff --git a/httpx/_decoders.py b/httpx/_decoders.py
index 180898c5..899dfada 100644
--- a/httpx/_decoders.py
+++ b/httpx/_decoders.py
@@ -175,9 +175,11 @@ class ZStandardDecoder(ContentDecoder):
) from None
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
+ self.seen_data = False
def decode(self, data: bytes) -> bytes:
assert zstandard is not None
+ self.seen_data = True
output = io.BytesIO()
try:
output.write(self.decompressor.decompress(data))
@@ -190,6 +192,8 @@ class ZStandardDecoder(ContentDecoder):
return output.getvalue()
def flush(self) -> bytes:
+ if not self.seen_data:
+ return b""
ret = self.decompressor.flush() # note: this is a no-op
if not self.decompressor.eof:
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index bcbb18bb..9ffaba18 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -100,6 +100,25 @@ def test_zstd_decoding_error():
)
+def test_zstd_empty():
+ headers = [(b"Content-Encoding", b"zstd")]
+ response = httpx.Response(200, headers=headers, content=b"")
+ assert response.content == b""
+
+
+def test_zstd_truncated():
+ body = b"test 123"
+ compressed_body = zstd.compress(body)
+
+ headers = [(b"Content-Encoding", b"zstd")]
+ with pytest.raises(httpx.DecodingError):
+ httpx.Response(
+ 200,
+ headers=headers,
+ content=compressed_body[1:3],
+ )
+
+
def test_zstd_multiframe():
# test inspired by urllib3 test suite
data = (
From ce7e14da27abba6574be9b3ea7cd5990556a9343 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 28 Nov 2024 11:46:59 +0000
Subject: [PATCH 3/9] Error on verify as str. (#3418)
---
httpx/_config.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/httpx/_config.py b/httpx/_config.py
index 9318de3c..1dec1bd3 100644
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -31,6 +31,14 @@ def create_ssl_context(verify: ssl.SSLContext | bool = True) -> ssl.SSLContext:
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
return ssl_context
+ elif isinstance(verify, str): # pragma: nocover
+ # Explicitly handle this deprecated usage pattern.
+ msg = (
+ "verify should be a boolean or SSLContext, since version 0.28. "
+ "Use `verify=ssl.create_default_context(cafile=...)` "
+ "or `verify=ssl.create_default_context(capath=...)`."
+ )
+ raise RuntimeError(msg)
return verify
From a33c87852b8a0dddc65e5f739af1e0a6fca4b91f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 28 Nov 2024 13:31:17 +0000
Subject: [PATCH 4/9] Fix `extensions` type annotation. (#3380)
---
httpx/_models.py | 4 ++--
httpx/_types.py | 5 ++---
2 files changed, 4 insertions(+), 5 deletions(-)
diff --git a/httpx/_models.py b/httpx/_models.py
index e7c992f7..67d74bf8 100644
--- a/httpx/_models.py
+++ b/httpx/_models.py
@@ -398,7 +398,7 @@ class Request:
self.method = method.upper()
self.url = URL(url) if params is None else URL(url, params=params)
self.headers = Headers(headers)
- self.extensions = {} if extensions is None else extensions
+ self.extensions = {} if extensions is None else dict(extensions)
if cookies:
Cookies(cookies).set_cookie_header(self)
@@ -537,7 +537,7 @@ class Response:
# the client will set `response.next_request`.
self.next_request: Request | None = None
- self.extensions: ResponseExtensions = {} if extensions is None else extensions
+ self.extensions = {} if extensions is None else dict(extensions)
self.history = [] if history is None else list(history)
self.is_closed = False
diff --git a/httpx/_types.py b/httpx/_types.py
index edd00da1..4f0eab96 100644
--- a/httpx/_types.py
+++ b/httpx/_types.py
@@ -15,7 +15,6 @@ from typing import (
Iterator,
List,
Mapping,
- MutableMapping,
Optional,
Sequence,
Tuple,
@@ -67,7 +66,7 @@ AuthTypes = Union[
RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
-ResponseExtensions = MutableMapping[str, Any]
+ResponseExtensions = Mapping[str, Any]
RequestData = Mapping[str, Any]
@@ -84,7 +83,7 @@ FileTypes = Union[
]
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
-RequestExtensions = MutableMapping[str, Any]
+RequestExtensions = Mapping[str, Any]
__all__ = ["AsyncByteStream", "SyncByteStream"]
From 80960fa31918d7663c3f4c3ad61661cf0e80628f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 28 Nov 2024 14:50:04 +0000
Subject: [PATCH 5/9] Version 0.28.0. (#3419)
---
CHANGELOG.md | 20 ++++++++--------
httpx/_api.py | 18 +++++++--------
httpx/_client.py | 41 ++++++++++++++++++++++++++------
httpx/_config.py | 45 ++++++++++++++++++++++++++++--------
httpx/_transports/default.py | 14 +++++++----
httpx/_types.py | 1 +
6 files changed, 98 insertions(+), 41 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e2afe2e..bc3fa411 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,28 +4,26 @@ 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/).
-## 0.28.0 (...)
+## 0.28.0 (28th November, 2024)
-The 0.28 release includes a limited set of backwards incompatible changes.
+The 0.28 release includes a limited set of deprecations.
-**Backwards incompatible changes**:
+**Deprecations**:
-SSL configuration has been significantly simplified.
+We are working towards a simplified SSL configuration API.
-* The `verify` argument no longer accepts string arguments.
-* The `cert` argument has now been removed.
-* The `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are no longer automatically used.
+*For users of the standard `verify=True` or `verify=False` cases, or `verify=` case this should require no changes. The following cases have been deprecated...*
-For users of the standard `verify=True` or `verify=False` cases this should require no changes.
+* The `verify` argument as a string argument is now deprecated and will raise warnings.
+* The `cert` argument is now deprecated and will raise warnings.
-For information on configuring more complex SSL cases, please see the [SSL documentation](docs/advanced/ssl.md).
+Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement the same behaviour with a more constrained API.
**The following changes are also included**:
-* The undocumented `URL.raw` property has now been deprecated, and will raise warnings.
* The deprecated `proxies` argument has now been removed.
* The deprecated `app` argument has now been removed.
-* Ensure JSON request bodies are compact. (#3363)
+* JSON request bodies use a compact representation. (#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)
diff --git a/httpx/_api.py b/httpx/_api.py
index ab1be081..c3cda1ec 100644
--- a/httpx/_api.py
+++ b/httpx/_api.py
@@ -51,7 +51,7 @@ def request(
proxy: ProxyTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
) -> Response:
"""
@@ -136,7 +136,7 @@ def stream(
proxy: ProxyTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
) -> typing.Iterator[Response]:
"""
@@ -180,7 +180,7 @@ def get(
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
@@ -216,7 +216,7 @@ def options(
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
@@ -252,7 +252,7 @@ def head(
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
@@ -292,7 +292,7 @@ def post(
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
@@ -333,7 +333,7 @@ def put(
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
@@ -374,7 +374,7 @@ def patch(
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
@@ -412,7 +412,7 @@ def delete(
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
) -> Response:
"""
diff --git a/httpx/_client.py b/httpx/_client.py
index 76325c14..018d440c 100644
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -33,6 +33,7 @@ from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._types import (
AsyncByteStream,
AuthTypes,
+ CertTypes,
CookieTypes,
HeaderTypes,
ProxyTypes,
@@ -644,7 +645,9 @@ class Client(BaseClient):
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
proxy: ProxyTypes | None = None,
@@ -656,7 +659,6 @@ class Client(BaseClient):
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URL | str = "",
transport: BaseTransport | None = None,
- trust_env: bool = True,
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None:
super().__init__(
@@ -687,6 +689,8 @@ class Client(BaseClient):
self._transport = self._init_transport(
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -698,6 +702,8 @@ class Client(BaseClient):
else self._init_proxy_transport(
proxy,
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -713,7 +719,9 @@ class Client(BaseClient):
def _init_transport(
self,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
@@ -724,6 +732,8 @@ class Client(BaseClient):
return HTTPTransport(
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -732,13 +742,17 @@ class Client(BaseClient):
def _init_proxy_transport(
self,
proxy: Proxy,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
) -> BaseTransport:
return HTTPTransport(
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -1345,7 +1359,8 @@ class AsyncClient(BaseClient):
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
http1: bool = True,
http2: bool = False,
proxy: ProxyTypes | None = None,
@@ -1388,6 +1403,8 @@ class AsyncClient(BaseClient):
self._transport = self._init_transport(
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -1400,6 +1417,8 @@ class AsyncClient(BaseClient):
else self._init_proxy_transport(
proxy,
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -1414,7 +1433,9 @@ class AsyncClient(BaseClient):
def _init_transport(
self,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
@@ -1425,6 +1446,8 @@ class AsyncClient(BaseClient):
return AsyncHTTPTransport(
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
@@ -1433,13 +1456,17 @@ class AsyncClient(BaseClient):
def _init_proxy_transport(
self,
proxy: Proxy,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
) -> AsyncBaseTransport:
return AsyncHTTPTransport(
verify=verify,
+ cert=cert,
+ trust_env=trust_env,
http1=http1,
http2=http2,
limits=limits,
diff --git a/httpx/_config.py b/httpx/_config.py
index 1dec1bd3..dbd2b46c 100644
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -1,9 +1,10 @@
from __future__ import annotations
+import os
import typing
from ._models import Headers
-from ._types import HeaderTypes, TimeoutTypes
+from ._types import CertTypes, HeaderTypes, TimeoutTypes
from ._urls import URL
if typing.TYPE_CHECKING:
@@ -19,28 +20,54 @@ class UnsetType:
UNSET = UnsetType()
-def create_ssl_context(verify: ssl.SSLContext | bool = True) -> ssl.SSLContext:
+def create_ssl_context(
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
+) -> ssl.SSLContext:
import ssl
+ import warnings
import certifi
if verify is True:
- return ssl.create_default_context(cafile=certifi.where())
+ if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover
+ ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
+ elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover
+ ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
+ else:
+ # Default case...
+ ctx = ssl.create_default_context(cafile=certifi.where())
elif verify is False:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
return ssl_context
elif isinstance(verify, str): # pragma: nocover
- # Explicitly handle this deprecated usage pattern.
- msg = (
- "verify should be a boolean or SSLContext, since version 0.28. "
+ message = (
+ "`verify=` is deprecated. "
"Use `verify=ssl.create_default_context(cafile=...)` "
- "or `verify=ssl.create_default_context(capath=...)`."
+ "or `verify=ssl.create_default_context(capath=...)` instead."
)
- raise RuntimeError(msg)
+ warnings.warn(message, DeprecationWarning)
+ if os.path.isdir(verify):
+ return ssl.create_default_context(capath=verify)
+ return ssl.create_default_context(cafile=verify)
+ else:
+ ctx = verify
- return verify
+ if cert: # pragma: nocover
+ message = (
+ "`cert=...` is deprecated. Use `verify=` instead,"
+ "with `.load_cert_chain()` to configure the certificate chain."
+ )
+ warnings.warn(message, DeprecationWarning)
+ if isinstance(cert, str):
+ ctx.load_cert_chain(cert)
+ else:
+ ctx.load_cert_chain(*cert)
+
+ return ctx
class Timeout:
diff --git a/httpx/_transports/default.py b/httpx/_transports/default.py
index ccc19af4..d5aa05ff 100644
--- a/httpx/_transports/default.py
+++ b/httpx/_transports/default.py
@@ -53,7 +53,7 @@ from .._exceptions import (
WriteTimeout,
)
from .._models import Request, Response
-from .._types import AsyncByteStream, ProxyTypes, SyncByteStream
+from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream
from .._urls import URL
from .base import AsyncBaseTransport, BaseTransport
@@ -135,7 +135,9 @@ class ResponseStream(SyncByteStream):
class HTTPTransport(BaseTransport):
def __init__(
self,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
@@ -148,7 +150,7 @@ class HTTPTransport(BaseTransport):
import httpcore
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
- ssl_context = create_ssl_context(verify=verify)
+ ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
if proxy is None:
self._pool = httpcore.ConnectionPool(
@@ -277,7 +279,9 @@ class AsyncResponseStream(AsyncByteStream):
class AsyncHTTPTransport(AsyncBaseTransport):
def __init__(
self,
- verify: ssl.SSLContext | bool = True,
+ verify: ssl.SSLContext | str | bool = True,
+ cert: CertTypes | None = None,
+ trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
@@ -290,7 +294,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
import httpcore
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
- ssl_context = create_ssl_context(verify=verify)
+ ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
if proxy is None:
self._pool = httpcore.AsyncConnectionPool(
diff --git a/httpx/_types.py b/httpx/_types.py
index 4f0eab96..704dfdff 100644
--- a/httpx/_types.py
+++ b/httpx/_types.py
@@ -57,6 +57,7 @@ TimeoutTypes = Union[
"Timeout",
]
ProxyTypes = Union["URL", str, "Proxy"]
+CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]]
AuthTypes = Union[
Tuple[Union[str, bytes], Union[str, bytes]],
From 15e21e9ea3cad4f06e22a7e704aabefdf43d2e29 Mon Sep 17 00:00:00 2001
From: Daniel Arvelini
Date: Fri, 29 Nov 2024 08:15:56 -0300
Subject: [PATCH 6/9] Updating deprecated docstring Client() class (#3426)
---
httpx/_client.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/httpx/_client.py b/httpx/_client.py
index 018d440c..2249231f 100644
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -620,8 +620,6 @@ class Client(BaseClient):
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
enabled. Defaults to `False`.
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
- * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy
- URLs.
* **timeout** - *(optional)* The timeout configuration to use when sending
requests.
* **limits** - *(optional)* The limits configuration to use.
From 0cb7e5a2e736628e2f506d259fcf0d48cd2bde82 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 3 Dec 2024 08:37:45 +0100
Subject: [PATCH 7/9] Bump the python-packages group with 11 updates (#3434)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marcelo Trylesinski
---
requirements.txt | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 53fd0a6c..0d8ba2ef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,19 +11,20 @@ chardet==5.2.0
# Documentation
mkdocs==1.6.1
mkautodoc==0.2.0
-mkdocs-material==9.5.39
+mkdocs-material==9.5.47
# Packaging
-build==1.2.2
-twine==5.1.1
+build==1.2.2.post1
+twine==6.0.1
# Tests & Linting
coverage[toml]==7.6.1
-cryptography==43.0.1
-mypy==1.11.2
-pytest==8.3.3
-ruff==0.6.8
-trio==0.26.2
+cryptography==44.0.0
+mypy==1.13.0
+pytest==8.3.4
+ruff==0.8.1
+trio==0.27.0
trio-typing==0.10.0
-trustme==1.1.0
-uvicorn==0.31.0
+trustme==1.1.0; python_version < '3.9'
+trustme==1.2.0; python_version >= '3.9'
+uvicorn==0.32.1
From 8ecb86f0d74ffc52d4663214fae9526bee89358d Mon Sep 17 00:00:00 2001
From: Elaina
Date: Wed, 4 Dec 2024 00:12:27 +0800
Subject: [PATCH 8/9] Add test for request params behavior changes (#3364)
(#3440)
Co-authored-by: Tom Christie
---
CHANGELOG.md | 1 +
tests/models/test_requests.py | 13 +++++++++++++
2 files changed, 14 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc3fa411..060013b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement th
* Ensure `certifi` and `httpcore` are only imported if required. (#3377)
* Treat `socks5h` as a valid proxy scheme. (#3178)
* Cleanup `Request()` method signature in line with `client.request()` and `httpx.request()`. (#3378)
+* Bugfix: When passing `params={}`, always strictly update rather than merge with an existing querystring. (#3364)
## 0.27.2 (27th August, 2024)
diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py
index d2a458d5..b31fe007 100644
--- a/tests/models/test_requests.py
+++ b/tests/models/test_requests.py
@@ -226,3 +226,16 @@ def test_request_generator_content_picklable():
request.read()
pickle_request = pickle.loads(pickle.dumps(request))
assert pickle_request.content == b"test 123"
+
+
+def test_request_params():
+ request = httpx.Request("GET", "http://example.com", params={})
+ assert str(request.url) == "http://example.com"
+
+ request = httpx.Request(
+ "GET", "http://example.com?c=3", params={"a": "1", "b": "2"}
+ )
+ assert str(request.url) == "http://example.com?a=1&b=2"
+
+ request = httpx.Request("GET", "http://example.com?a=1", params={})
+ assert str(request.url) == "http://example.com"
From 89599a9541af14bcf906fc4ed58ccbdf403802ba Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 4 Dec 2024 11:29:09 +0000
Subject: [PATCH 9/9] Fix `verify=False`, `cert=...` case. (#3442)
---
CHANGELOG.md | 4 ++++
httpx/_config.py | 7 +++----
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 060013b0..4f1233ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ 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/).
+## Dev
+
+* Fix SSL case where `verify=False` together with client side certificates.
+
## 0.28.0 (28th November, 2024)
The 0.28 release includes a limited set of deprecations.
diff --git a/httpx/_config.py b/httpx/_config.py
index dbd2b46c..467a6c90 100644
--- a/httpx/_config.py
+++ b/httpx/_config.py
@@ -39,10 +39,9 @@ def create_ssl_context(
# Default case...
ctx = ssl.create_default_context(cafile=certifi.where())
elif verify is False:
- ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
- ssl_context.check_hostname = False
- ssl_context.verify_mode = ssl.CERT_NONE
- return ssl_context
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
elif isinstance(verify, str): # pragma: nocover
message = (
"`verify=` is deprecated. "