Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-httpcore2 for
openSUSE:Factory checked in at 2026-08-27 18:50:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httpcore2 (Old)
and /work/SRC/openSUSE:Factory/.python-httpcore2.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-httpcore2"
Thu Aug 27 18:50:38 2026 rev:3 rq:1373646 version:2.12.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httpcore2/python-httpcore2.changes
2026-07-06 12:27:33.189440892 +0200
+++
/work/SRC/openSUSE:Factory/.python-httpcore2.new.1265/python-httpcore2.changes
2026-08-27 18:53:14.884475761 +0200
@@ -1,0 +2,20 @@
+Tue Aug 25 15:19:19 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2.12.0:
+ * No changes since `2.11.0`. Version bumped to stay in lockstep
+ with `httpx2`.
+ * Cache sniffio availability instead of importing it on every
+ synchronization call.
+ * Add support for Python 3.15.
+ * Avoid quadratic copying when sending large HTTP/2 request
+ bodies.
+ * Propagate the original exception instead of raising
+ `KeyError` when an HTTP/2 stream fails.
+ * Start TLS for the `wss` scheme in SOCKS5 proxy connections.
+ * Assign each released connection to a single queued request,
+ eliminating multi-assignment churn in the connection pool.
+ * Clean up garbage connections on cancellation in
+ `AsyncConnectionPool`.
+ * Read `_expire_at` only once in `has_expired()`.
+
+-------------------------------------------------------------------
Old:
----
httpcore2-2.5.0.tar.gz
New:
----
httpcore2-2.12.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-httpcore2.spec ++++++
--- /var/tmp/diff_new_pack.sCMkD7/_old 2026-08-27 18:53:15.791507364 +0200
+++ /var/tmp/diff_new_pack.sCMkD7/_new 2026-08-27 18:53:15.793507434 +0200
@@ -17,7 +17,7 @@
Name: python-httpcore2
-Version: 2.5.0
+Version: 2.12.0
Release: 0
Summary: A minimal low-level HTTP client
License: BSD-3-Clause
@@ -26,7 +26,7 @@
BuildRequires: %{python_module hatch-fancy-pypi-readme}
BuildRequires: %{python_module hatchling}
BuildRequires: %{python_module pip}
-BuildRequires: %{python_module uv-dynamic-versioning >= 0.8.0}
+BuildRequires: %{python_module uv-dynamic-versioning >= 0.14.0}
BuildRequires: python-rpm-macros
# SECTION test requirements
BuildRequires: %{python_module h11 >= 0.16}
@@ -37,7 +37,7 @@
Requires: python-truststore >= 0.10
Suggests: python-h2 >= 3
Suggests: python-socksio >= 1.0
-Suggests: python-trio >= 0.22.0
+Suggests: python-trio >= 0.33.0
Suggests: python-anyio >= 4.5
BuildArch: noarch
%python_subpackages
++++++ httpcore2-2.5.0.tar.gz -> httpcore2-2.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/CHANGELOG.md
new/httpcore2-2.12.0/CHANGELOG.md
--- old/httpcore2-2.5.0/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/CHANGELOG.md 2020-02-02 01:00:00.000000000 +0100
@@ -4,6 +4,57 @@
The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/).
+## 2.12.0 (August 18th, 2026)
+
+No changes since `2.11.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.11.0 (August 18th, 2026)
+
+### Changed
+
+* Cache sniffio availability instead of importing it on every synchronization
call.
+ ([#1132](https://github.com/pydantic/httpx2/pull/1132))
+
+## 2.10.0 (August 9th, 2026)
+
+### Added
+
+* Add support for Python 3.15.
([#1090](https://github.com/pydantic/httpx2/pull/1090))
+
+### Changed
+
+* Avoid quadratic copying when sending large HTTP/2 request bodies.
([#1127](https://github.com/pydantic/httpx2/pull/1127))
+
+### Fixed
+
+* Propagate the original exception instead of raising `KeyError` when an
HTTP/2 stream fails. ([#1093](https://github.com/pydantic/httpx2/pull/1093))
+* Start TLS for the `wss` scheme in SOCKS5 proxy connections.
([#1104](https://github.com/pydantic/httpx2/pull/1104))
+
+## 2.9.1 (July 24th, 2026)
+
+No changes since `2.9.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.9.0 (July 23rd, 2026)
+
+No changes since `2.8.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.8.0 (July 23rd, 2026)
+
+### Changed
+
+* Assign each released connection to a single queued request, eliminating
multi-assignment churn in the connection pool.
([#1075](https://github.com/pydantic/httpx2/pull/1075))
+
+## 2.7.0 (July 14th, 2026)
+
+No changes since `2.6.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.6.0 (July 14th, 2026)
+
+### Fixed
+
+* Clean up garbage connections on cancellation in `AsyncConnectionPool`.
([#983](https://github.com/pydantic/httpx2/pull/983))
+* Read `_expire_at` only once in `has_expired()`.
([#1045](https://github.com/pydantic/httpx2/pull/1045))
+
## 2.5.0 (June 25th, 2026)
### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/PKG-INFO
new/httpcore2-2.12.0/PKG-INFO
--- old/httpcore2-2.5.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
-Metadata-Version: 2.4
+Metadata-Version: 2.5
Name: httpcore2
-Version: 2.5.0
+Version: 2.12.0
Summary: A minimal low-level HTTP client.
Project-URL: Changelog,
https://github.com/pydantic/httpx2/blob/main/src/httpcore2/CHANGELOG.md
Project-URL: Homepage, https://github.com/pydantic/httpx2
@@ -24,6 +24,7 @@
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Requires-Dist: h11>=0.16
@@ -35,17 +36,17 @@
Provides-Extra: socks
Requires-Dist: socksio==1.*; extra == 'socks'
Provides-Extra: trio
-Requires-Dist: trio<1.0,>=0.22.0; extra == 'trio'
+Requires-Dist: trio<1.0,>=0.33.0; extra == 'trio'
Description-Content-Type: text/markdown
-# HTTP Core
+# HTTPCore 2
-[](https://github.com/encode/httpcore/actions)
-[](https://pypi.org/project/httpcore/)
+[](https://github.com/pydantic/httpx2/actions)
+[](https://pypi.org/project/httpcore2/)
> *Do one thing, and do it well.*
-The HTTP Core package provides a minimal low-level HTTP client, which does
+The HTTPCore 2 package provides a minimal low-level HTTP client, which does
one thing only. Sending HTTP requests.
It does not provide any high level model abstractions over the API,
@@ -54,7 +55,7 @@
content or charset decoding, handling JSON, environment based configuration
defaults, or any of that Jazz.
-Some things HTTP Core does do:
+Some things HTTPCore 2 does do:
* Sending HTTP requests.
* Thread-safe / task-safe connection pooling.
@@ -68,13 +69,13 @@
For HTTP/1.1 only support, install with:
```shell
-pip install httpcore
+pip install httpcore2
```
There are also a number of optional extras available...
```shell
-pip install httpcore['asyncio,trio,http2,socks']
+pip install 'httpcore2[asyncio,trio,http2,socks]'
```
## Sending requests
@@ -82,9 +83,9 @@
Send an HTTP request:
```python
-import httpcore
+import httpcore2
-response = httpcore.request("GET", "https://www.example.com/")
+response = httpcore2.request("GET", "https://www.example.com/")
print(response)
# <Response [200]>
@@ -96,25 +97,25 @@
# b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>\n\n<meta
charset="utf-8"/>\n ...'
```
-The top-level `httpcore.request()` function is provided for convenience. In
practice whenever you're working with `httpcore` you'll want to use the
connection pooling functionality that it provides.
+The top-level `httpcore2.request()` function is provided for convenience. In
practice whenever you're working with `httpcore2` you'll want to use the
connection pooling functionality that it provides.
```python
-import httpcore
+import httpcore2
-http = httpcore.ConnectionPool()
+http = httpcore2.ConnectionPool()
response = http.request("GET", "https://www.example.com/")
```
-Once you're ready to get going, [head over to the
documentation](https://www.encode.io/httpcore/).
+Once you're ready to get going, [head over to the
documentation](https://github.com/pydantic/httpx2/tree/main/src/httpcore2/docs).
## Motivation
-You *probably* don't want to be using HTTP Core directly. It might make sense
if
+You *probably* don't want to be using HTTPCore 2 directly. It might make sense
if
you're writing something like a proxy service in Python, and you just want
something at the lowest possible level, but more typically you'll want to use
-a higher level client library, such as `httpx`.
+a higher level client library, such as `httpx2`.
-The motivation for `httpcore` is:
+The motivation for `httpcore2` is:
* To provide a reusable low-level client library, that other packages can then
build on top of.
* To provide a *really clear interface split* between the networking code and
client logic,
@@ -122,17 +123,17 @@
## Dependencies
-The `httpcore` package has the following dependencies...
+The `httpcore2` package has the following dependencies...
* `h11`
* `truststore`
And the following optional extras...
-* `anyio` - Required by `pip install httpcore['asyncio']`.
-* `trio` - Required by `pip install httpcore['trio']`.
-* `h2` - Required by `pip install httpcore['http2']`.
-* `socksio` - Required by `pip install httpcore['socks']`.
+* `anyio` - Required by `pip install 'httpcore2[asyncio]'`.
+* `trio` - Required by `pip install 'httpcore2[trio]'`.
+* `h2` - Required by `pip install 'httpcore2[http2]'`.
+* `socksio` - Required by `pip install 'httpcore2[socks]'`.
## Versioning
@@ -142,8 +143,8 @@
We recommend pinning your requirements either the most current major version,
or a more specific version range:
-```python
-pip install 'httpcore==1.*'
+```shell
+pip install 'httpcore2==2.*'
```
# Changelog
@@ -151,6 +152,57 @@
The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/).
+## 2.12.0 (August 18th, 2026)
+
+No changes since `2.11.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.11.0 (August 18th, 2026)
+
+### Changed
+
+* Cache sniffio availability instead of importing it on every synchronization
call.
+ ([#1132](https://github.com/pydantic/httpx2/pull/1132))
+
+## 2.10.0 (August 9th, 2026)
+
+### Added
+
+* Add support for Python 3.15.
([#1090](https://github.com/pydantic/httpx2/pull/1090))
+
+### Changed
+
+* Avoid quadratic copying when sending large HTTP/2 request bodies.
([#1127](https://github.com/pydantic/httpx2/pull/1127))
+
+### Fixed
+
+* Propagate the original exception instead of raising `KeyError` when an
HTTP/2 stream fails. ([#1093](https://github.com/pydantic/httpx2/pull/1093))
+* Start TLS for the `wss` scheme in SOCKS5 proxy connections.
([#1104](https://github.com/pydantic/httpx2/pull/1104))
+
+## 2.9.1 (July 24th, 2026)
+
+No changes since `2.9.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.9.0 (July 23rd, 2026)
+
+No changes since `2.8.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.8.0 (July 23rd, 2026)
+
+### Changed
+
+* Assign each released connection to a single queued request, eliminating
multi-assignment churn in the connection pool.
([#1075](https://github.com/pydantic/httpx2/pull/1075))
+
+## 2.7.0 (July 14th, 2026)
+
+No changes since `2.6.0`. Version bumped to stay in lockstep with `httpx2`.
+
+## 2.6.0 (July 14th, 2026)
+
+### Fixed
+
+* Clean up garbage connections on cancellation in `AsyncConnectionPool`.
([#983](https://github.com/pydantic/httpx2/pull/983))
+* Read `_expire_at` only once in `has_expired()`.
([#1045](https://github.com/pydantic/httpx2/pull/1045))
+
## 2.5.0 (June 25th, 2026)
### Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/README.md
new/httpcore2-2.12.0/README.md
--- old/httpcore2-2.5.0/README.md 2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/README.md 2020-02-02 01:00:00.000000000 +0100
@@ -1,11 +1,11 @@
-# HTTP Core
+# HTTPCore 2
-[](https://github.com/encode/httpcore/actions)
-[](https://pypi.org/project/httpcore/)
+[](https://github.com/pydantic/httpx2/actions)
+[](https://pypi.org/project/httpcore2/)
> *Do one thing, and do it well.*
-The HTTP Core package provides a minimal low-level HTTP client, which does
+The HTTPCore 2 package provides a minimal low-level HTTP client, which does
one thing only. Sending HTTP requests.
It does not provide any high level model abstractions over the API,
@@ -14,7 +14,7 @@
content or charset decoding, handling JSON, environment based configuration
defaults, or any of that Jazz.
-Some things HTTP Core does do:
+Some things HTTPCore 2 does do:
* Sending HTTP requests.
* Thread-safe / task-safe connection pooling.
@@ -28,13 +28,13 @@
For HTTP/1.1 only support, install with:
```shell
-pip install httpcore
+pip install httpcore2
```
There are also a number of optional extras available...
```shell
-pip install httpcore['asyncio,trio,http2,socks']
+pip install 'httpcore2[asyncio,trio,http2,socks]'
```
## Sending requests
@@ -42,9 +42,9 @@
Send an HTTP request:
```python
-import httpcore
+import httpcore2
-response = httpcore.request("GET", "https://www.example.com/")
+response = httpcore2.request("GET", "https://www.example.com/")
print(response)
# <Response [200]>
@@ -56,25 +56,25 @@
# b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>\n\n<meta
charset="utf-8"/>\n ...'
```
-The top-level `httpcore.request()` function is provided for convenience. In
practice whenever you're working with `httpcore` you'll want to use the
connection pooling functionality that it provides.
+The top-level `httpcore2.request()` function is provided for convenience. In
practice whenever you're working with `httpcore2` you'll want to use the
connection pooling functionality that it provides.
```python
-import httpcore
+import httpcore2
-http = httpcore.ConnectionPool()
+http = httpcore2.ConnectionPool()
response = http.request("GET", "https://www.example.com/")
```
-Once you're ready to get going, [head over to the
documentation](https://www.encode.io/httpcore/).
+Once you're ready to get going, [head over to the
documentation](https://github.com/pydantic/httpx2/tree/main/src/httpcore2/docs).
## Motivation
-You *probably* don't want to be using HTTP Core directly. It might make sense
if
+You *probably* don't want to be using HTTPCore 2 directly. It might make sense
if
you're writing something like a proxy service in Python, and you just want
something at the lowest possible level, but more typically you'll want to use
-a higher level client library, such as `httpx`.
+a higher level client library, such as `httpx2`.
-The motivation for `httpcore` is:
+The motivation for `httpcore2` is:
* To provide a reusable low-level client library, that other packages can then
build on top of.
* To provide a *really clear interface split* between the networking code and
client logic,
@@ -82,17 +82,17 @@
## Dependencies
-The `httpcore` package has the following dependencies...
+The `httpcore2` package has the following dependencies...
* `h11`
* `truststore`
And the following optional extras...
-* `anyio` - Required by `pip install httpcore['asyncio']`.
-* `trio` - Required by `pip install httpcore['trio']`.
-* `h2` - Required by `pip install httpcore['http2']`.
-* `socksio` - Required by `pip install httpcore['socks']`.
+* `anyio` - Required by `pip install 'httpcore2[asyncio]'`.
+* `trio` - Required by `pip install 'httpcore2[trio]'`.
+* `h2` - Required by `pip install 'httpcore2[http2]'`.
+* `socksio` - Required by `pip install 'httpcore2[socks]'`.
## Versioning
@@ -102,6 +102,6 @@
We recommend pinning your requirements either the most current major version,
or a more specific version range:
-```python
-pip install 'httpcore==1.*'
+```shell
+pip install 'httpcore2==2.*'
```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/connection.py
new/httpcore2-2.12.0/httpcore2/_async/connection.py
--- old/httpcore2-2.5.0/httpcore2/_async/connection.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/connection.py 2020-02-02
01:00:00.000000000 +0100
@@ -156,6 +156,9 @@
async with Trace("close", logger, None, {}):
await self._connection.aclose()
+ def is_connected(self) -> bool:
+ return self._connection is not None and self._connection.is_connected()
+
def is_available(self) -> bool:
if self._connection is None:
# If HTTP/2 support is enabled, and the resulting connection could
@@ -174,6 +177,9 @@
return self._connect_failed
return self._connection.is_idle()
+ def can_multiplex(self) -> bool:
+ return self._connection is not None and
self._connection.can_multiplex()
+
def is_closed(self) -> bool:
if self._connection is None:
return self._connect_failed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/connection_pool.py
new/httpcore2-2.12.0/httpcore2/_async/connection_pool.py
--- old/httpcore2-2.5.0/httpcore2/_async/connection_pool.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/connection_pool.py 2020-02-02
01:00:00.000000000 +0100
@@ -264,25 +264,40 @@
closing_connections: list[AsyncConnectionInterface] = []
retained_connections: list[AsyncConnectionInterface] = []
+ # Connections currently referenced by an in-flight request, including
+ # connections that are in the process of being established and idle
+ # connections reserved by an assigned-but-not-yet-sent request.
+ request_connections = {r.connection for r in self._requests}
+
# First we handle cleaning up any connections that are closed
- # or have expired their keep-alive, in a single pass.
+ # or have expired their keep-alive, in a single pass. Reserved
+ # connections skip the expiry check: they were checked when assigned,
+ # and `has_expired()` on an idle connection probes the socket.
for connection in self._connections:
+ reserved = connection in request_connections
if connection.is_closed():
continue
- elif connection.has_expired():
+ elif not (connection.is_connected() or reserved):
+ # Garbage: a NEW-state connection whose request was cancelled
+ # before the TCP handshake completed. Drop it without closing
+ # (there is no socket to close yet).
+ continue
+ elif not reserved and connection.has_expired():
closing_connections.append(connection)
else:
retained_connections.append(connection)
# Then we close any surplus idle connections, to enforce the
- # max_keepalive_connections setting.
+ # max_keepalive_connections setting. Reserved connections are not
+ # surplus: a request is about to be sent on them.
idle_surplus = (
- sum(connection.is_idle() for connection in retained_connections) -
self._max_keepalive_connections
+ sum(connection.is_idle() and connection not in request_connections
for connection in retained_connections)
+ - self._max_keepalive_connections
)
if idle_surplus > 0:
kept: list[AsyncConnectionInterface] = []
for connection in retained_connections:
- if idle_surplus > 0 and connection.is_idle():
+ if idle_surplus > 0 and connection.is_idle() and connection
not in request_connections:
closing_connections.append(connection)
idle_surplus -= 1
else:
@@ -294,11 +309,27 @@
# Snapshot the set of reusable connections once, rather than rebuilding
# it per queued request — this is what brings the loop from O(N*M) to
# O(N+M) in the common case.
- available_connections = [connection for connection in
self._connections if connection.is_available()]
+ #
+ # An idle connection already assigned to an in-flight request is
+ # reserved: it stays IDLE until the winning task sends on it, so
+ # without this exclusion the next pass would assign it again and the
+ # loser would churn through `ConnectionNotAvailable`. Multiplexing
+ # connections are exempt: they can take further requests while idle.
+ available_connections = [
+ connection
+ for connection in self._connections
+ if connection.is_available()
+ and not (connection.is_idle() and connection in
request_connections and not connection.can_multiplex())
+ ]
new_connection_budget = self._max_connections - len(self._connections)
- # Assign queued requests to connections.
+ # Assign queued requests to connections. Once no connection is
+ # available and no new connection may be created, no queued request
+ # can be assigned, so the scan stops early: this keeps a pass on a
+ # saturated pool O(connections) rather than O(in-flight requests).
for pool_request in self._requests:
+ if not available_connections and new_connection_budget <= 0:
+ break
if not pool_request.is_queued():
continue
origin = pool_request.request.url.origin
@@ -309,9 +340,13 @@
# 2. We can create a new connection to handle the request.
# 3. We can close an idle connection and then create a new
connection
# to handle the request.
- for connection in available_connections:
+ for idx, connection in enumerate(available_connections):
if connection.can_handle_request(origin):
pool_request.assign_to_connection(connection)
+ if connection.is_idle() and not connection.can_multiplex():
+ # An idle HTTP/1.1 connection can only take this
+ # single request until it is released.
+ del available_connections[idx]
break
else:
if new_connection_budget > 0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/http11.py
new/httpcore2-2.12.0/httpcore2/_async/http11.py
--- old/httpcore2-2.5.0/httpcore2/_async/http11.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/http11.py 2020-02-02
01:00:00.000000000 +0100
@@ -242,6 +242,9 @@
def can_handle_request(self, origin: Origin) -> bool:
return origin == self._origin
+ def is_connected(self) -> bool:
+ return not self.is_closed()
+
def is_available(self) -> bool:
# Note that HTTP/1.1 connections in the "NEW" state are not treated as
# being "available". The control flow which created the connection will
@@ -251,7 +254,10 @@
def has_expired(self) -> bool:
now = time.monotonic()
- keepalive_expired = self._expire_at is not None and now >
self._expire_at
+ # Read `_expire_at` once into a local: on free-threaded builds another
+ # thread may reset it to `None` between the check and the comparison.
+ expire_at = self._expire_at
+ keepalive_expired = expire_at is not None and now > expire_at
# If the HTTP connection is idle but the socket is readable, then the
# only valid state is that the socket is about to return b"",
indicating
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/http2.py
new/httpcore2-2.12.0/httpcore2/_async/http2.py
--- old/httpcore2-2.5.0/httpcore2/_async/http2.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/http2.py 2020-02-02
01:00:00.000000000 +0100
@@ -246,10 +246,11 @@
"""
Send a single chunk of data in one or more data frames.
"""
- while data:
+ position = 0
+ while position < len(data):
max_flow = await self._wait_for_outgoing_flow(request, stream_id)
- chunk_size = min(len(data), max_flow)
- chunk, data = data[:chunk_size], data[chunk_size:]
+ chunk = data[position : position + max_flow]
+ position += len(chunk)
self._h2_state.send_data(stream_id, chunk)
await self._write_outgoing_data(request)
@@ -373,9 +374,10 @@
self._max_streams -= 1
async def _response_closed(self, stream_id: int) -> None:
- await self._max_streams_semaphore.release()
async with self._state_lock:
- del self._events[stream_id]
+ if stream_id in self._events:
+ await self._max_streams_semaphore.release()
+ del self._events[stream_id]
if self._connection_terminated and not self._events:
await self.aclose()
@@ -474,6 +476,9 @@
def can_handle_request(self, origin: Origin) -> bool:
return origin == self._origin
+ def is_connected(self) -> bool:
+ return not self.is_closed()
+
def is_available(self) -> bool:
return (
self._state != HTTPConnectionState.CLOSED
@@ -484,11 +489,17 @@
def has_expired(self) -> bool:
now = time.monotonic()
- return self._expire_at is not None and now > self._expire_at
+ # Read `_expire_at` once into a local: on free-threaded builds another
+ # thread may reset it to `None` between the check and the comparison.
+ expire_at = self._expire_at
+ return expire_at is not None and now > expire_at
def is_idle(self) -> bool:
return self._state == HTTPConnectionState.IDLE
+ def can_multiplex(self) -> bool:
+ return True
+
def is_closed(self) -> bool:
return self._state == HTTPConnectionState.CLOSED
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/http_proxy.py
new/httpcore2-2.12.0/httpcore2/_async/http_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_async/http_proxy.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/http_proxy.py 2020-02-02
01:00:00.000000000 +0100
@@ -204,6 +204,9 @@
def info(self) -> str:
return self._connection.info()
+ def is_connected(self) -> bool:
+ return self._connection.is_connected()
+
def is_available(self) -> bool:
return self._connection.is_available()
@@ -330,6 +333,9 @@
def info(self) -> str:
return self._connection.info()
+ def is_connected(self) -> bool:
+ return self._connection.is_connected()
+
def is_available(self) -> bool:
return self._connection.is_available()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/interfaces.py
new/httpcore2-2.12.0/httpcore2/_async/interfaces.py
--- old/httpcore2-2.5.0/httpcore2/_async/interfaces.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/interfaces.py 2020-02-02
01:00:00.000000000 +0100
@@ -95,6 +95,18 @@
def can_handle_request(self, origin: Origin) -> bool:
raise NotImplementedError() # pragma: no cover
+ def is_connected(self) -> bool:
+ """
+ Return `True` if the connection is open (the underlying socket has been
+ established). A connection in the NEW state (just created but not yet
+ connected) returns `False`.
+
+ Note: for some implementations `is_connected() != not is_closed()`.
+ The default implementation returns `not self.is_closed()`, which is
+ correct for connections that are never in the NEW (pre-TCP) state.
+ """
+ return not self.is_closed() # pragma: no cover
+
def is_available(self) -> bool:
"""
Return `True` if the connection is currently able to accept an
@@ -128,6 +140,16 @@
"""
raise NotImplementedError() # pragma: no cover
+ def can_multiplex(self) -> bool:
+ """
+ Return `True` if the connection can serve multiple requests
+ concurrently, such as an established HTTP/2 connection.
+
+ The default covers HTTP/1.1-style implementations, which serve a
+ single request at a time.
+ """
+ return False
+
def is_closed(self) -> bool:
"""
Return `True` if the connection has been closed.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_async/socks_proxy.py
new/httpcore2-2.12.0/httpcore2/_async/socks_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_async/socks_proxy.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_async/socks_proxy.py 2020-02-02
01:00:00.000000000 +0100
@@ -240,7 +240,7 @@
trace.return_value = stream
# Upgrade the stream to SSL
- if self._remote_origin.scheme == b"https":
+ if self._remote_origin.scheme in (b"https", b"wss"):
ssl_context = default_ssl_context() if
self._ssl_context is None else self._ssl_context
alpn_protocols = ["http/1.1", "h2"] if self._http2
else ["http/1.1"]
ssl_context.set_alpn_protocols(alpn_protocols)
@@ -288,6 +288,9 @@
if self._connection is not None:
await self._connection.aclose()
+ def is_connected(self) -> bool:
+ return self._connection is not None and self._connection.is_connected()
+
def is_available(self) -> bool:
if self._connection is None: # pragma: no cover
# If HTTP/2 support is enabled, and the resulting connection could
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/connection.py
new/httpcore2-2.12.0/httpcore2/_sync/connection.py
--- old/httpcore2-2.5.0/httpcore2/_sync/connection.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/connection.py 2020-02-02
01:00:00.000000000 +0100
@@ -156,6 +156,9 @@
with Trace("close", logger, None, {}):
self._connection.close()
+ def is_connected(self) -> bool:
+ return self._connection is not None and self._connection.is_connected()
+
def is_available(self) -> bool:
if self._connection is None:
# If HTTP/2 support is enabled, and the resulting connection could
@@ -174,6 +177,9 @@
return self._connect_failed
return self._connection.is_idle()
+ def can_multiplex(self) -> bool:
+ return self._connection is not None and
self._connection.can_multiplex()
+
def is_closed(self) -> bool:
if self._connection is None:
return self._connect_failed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/connection_pool.py
new/httpcore2-2.12.0/httpcore2/_sync/connection_pool.py
--- old/httpcore2-2.5.0/httpcore2/_sync/connection_pool.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/connection_pool.py 2020-02-02
01:00:00.000000000 +0100
@@ -264,25 +264,40 @@
closing_connections: list[ConnectionInterface] = []
retained_connections: list[ConnectionInterface] = []
+ # Connections currently referenced by an in-flight request, including
+ # connections that are in the process of being established and idle
+ # connections reserved by an assigned-but-not-yet-sent request.
+ request_connections = {r.connection for r in self._requests}
+
# First we handle cleaning up any connections that are closed
- # or have expired their keep-alive, in a single pass.
+ # or have expired their keep-alive, in a single pass. Reserved
+ # connections skip the expiry check: they were checked when assigned,
+ # and `has_expired()` on an idle connection probes the socket.
for connection in self._connections:
+ reserved = connection in request_connections
if connection.is_closed():
continue
- elif connection.has_expired():
+ elif not (connection.is_connected() or reserved):
+ # Garbage: a NEW-state connection whose request was cancelled
+ # before the TCP handshake completed. Drop it without closing
+ # (there is no socket to close yet).
+ continue
+ elif not reserved and connection.has_expired():
closing_connections.append(connection)
else:
retained_connections.append(connection)
# Then we close any surplus idle connections, to enforce the
- # max_keepalive_connections setting.
+ # max_keepalive_connections setting. Reserved connections are not
+ # surplus: a request is about to be sent on them.
idle_surplus = (
- sum(connection.is_idle() for connection in retained_connections) -
self._max_keepalive_connections
+ sum(connection.is_idle() and connection not in request_connections
for connection in retained_connections)
+ - self._max_keepalive_connections
)
if idle_surplus > 0:
kept: list[ConnectionInterface] = []
for connection in retained_connections:
- if idle_surplus > 0 and connection.is_idle():
+ if idle_surplus > 0 and connection.is_idle() and connection
not in request_connections:
closing_connections.append(connection)
idle_surplus -= 1
else:
@@ -294,11 +309,27 @@
# Snapshot the set of reusable connections once, rather than rebuilding
# it per queued request — this is what brings the loop from O(N*M) to
# O(N+M) in the common case.
- available_connections = [connection for connection in
self._connections if connection.is_available()]
+ #
+ # An idle connection already assigned to an in-flight request is
+ # reserved: it stays IDLE until the winning task sends on it, so
+ # without this exclusion the next pass would assign it again and the
+ # loser would churn through `ConnectionNotAvailable`. Multiplexing
+ # connections are exempt: they can take further requests while idle.
+ available_connections = [
+ connection
+ for connection in self._connections
+ if connection.is_available()
+ and not (connection.is_idle() and connection in
request_connections and not connection.can_multiplex())
+ ]
new_connection_budget = self._max_connections - len(self._connections)
- # Assign queued requests to connections.
+ # Assign queued requests to connections. Once no connection is
+ # available and no new connection may be created, no queued request
+ # can be assigned, so the scan stops early: this keeps a pass on a
+ # saturated pool O(connections) rather than O(in-flight requests).
for pool_request in self._requests:
+ if not available_connections and new_connection_budget <= 0:
+ break
if not pool_request.is_queued():
continue
origin = pool_request.request.url.origin
@@ -309,9 +340,13 @@
# 2. We can create a new connection to handle the request.
# 3. We can close an idle connection and then create a new
connection
# to handle the request.
- for connection in available_connections:
+ for idx, connection in enumerate(available_connections):
if connection.can_handle_request(origin):
pool_request.assign_to_connection(connection)
+ if connection.is_idle() and not connection.can_multiplex():
+ # An idle HTTP/1.1 connection can only take this
+ # single request until it is released.
+ del available_connections[idx]
break
else:
if new_connection_budget > 0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/http11.py
new/httpcore2-2.12.0/httpcore2/_sync/http11.py
--- old/httpcore2-2.5.0/httpcore2/_sync/http11.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/http11.py 2020-02-02
01:00:00.000000000 +0100
@@ -242,6 +242,9 @@
def can_handle_request(self, origin: Origin) -> bool:
return origin == self._origin
+ def is_connected(self) -> bool:
+ return not self.is_closed()
+
def is_available(self) -> bool:
# Note that HTTP/1.1 connections in the "NEW" state are not treated as
# being "available". The control flow which created the connection will
@@ -251,7 +254,10 @@
def has_expired(self) -> bool:
now = time.monotonic()
- keepalive_expired = self._expire_at is not None and now >
self._expire_at
+ # Read `_expire_at` once into a local: on free-threaded builds another
+ # thread may reset it to `None` between the check and the comparison.
+ expire_at = self._expire_at
+ keepalive_expired = expire_at is not None and now > expire_at
# If the HTTP connection is idle but the socket is readable, then the
# only valid state is that the socket is about to return b"",
indicating
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/http2.py
new/httpcore2-2.12.0/httpcore2/_sync/http2.py
--- old/httpcore2-2.5.0/httpcore2/_sync/http2.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/http2.py 2020-02-02
01:00:00.000000000 +0100
@@ -246,10 +246,11 @@
"""
Send a single chunk of data in one or more data frames.
"""
- while data:
+ position = 0
+ while position < len(data):
max_flow = self._wait_for_outgoing_flow(request, stream_id)
- chunk_size = min(len(data), max_flow)
- chunk, data = data[:chunk_size], data[chunk_size:]
+ chunk = data[position : position + max_flow]
+ position += len(chunk)
self._h2_state.send_data(stream_id, chunk)
self._write_outgoing_data(request)
@@ -373,9 +374,10 @@
self._max_streams -= 1
def _response_closed(self, stream_id: int) -> None:
- self._max_streams_semaphore.release()
with self._state_lock:
- del self._events[stream_id]
+ if stream_id in self._events:
+ self._max_streams_semaphore.release()
+ del self._events[stream_id]
if self._connection_terminated and not self._events:
self.close()
@@ -474,6 +476,9 @@
def can_handle_request(self, origin: Origin) -> bool:
return origin == self._origin
+ def is_connected(self) -> bool:
+ return not self.is_closed()
+
def is_available(self) -> bool:
return (
self._state != HTTPConnectionState.CLOSED
@@ -484,11 +489,17 @@
def has_expired(self) -> bool:
now = time.monotonic()
- return self._expire_at is not None and now > self._expire_at
+ # Read `_expire_at` once into a local: on free-threaded builds another
+ # thread may reset it to `None` between the check and the comparison.
+ expire_at = self._expire_at
+ return expire_at is not None and now > expire_at
def is_idle(self) -> bool:
return self._state == HTTPConnectionState.IDLE
+ def can_multiplex(self) -> bool:
+ return True
+
def is_closed(self) -> bool:
return self._state == HTTPConnectionState.CLOSED
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/http_proxy.py
new/httpcore2-2.12.0/httpcore2/_sync/http_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_sync/http_proxy.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/http_proxy.py 2020-02-02
01:00:00.000000000 +0100
@@ -204,6 +204,9 @@
def info(self) -> str:
return self._connection.info()
+ def is_connected(self) -> bool:
+ return self._connection.is_connected()
+
def is_available(self) -> bool:
return self._connection.is_available()
@@ -330,6 +333,9 @@
def info(self) -> str:
return self._connection.info()
+ def is_connected(self) -> bool:
+ return self._connection.is_connected()
+
def is_available(self) -> bool:
return self._connection.is_available()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/interfaces.py
new/httpcore2-2.12.0/httpcore2/_sync/interfaces.py
--- old/httpcore2-2.5.0/httpcore2/_sync/interfaces.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/interfaces.py 2020-02-02
01:00:00.000000000 +0100
@@ -95,6 +95,18 @@
def can_handle_request(self, origin: Origin) -> bool:
raise NotImplementedError() # pragma: no cover
+ def is_connected(self) -> bool:
+ """
+ Return `True` if the connection is open (the underlying socket has been
+ established). A connection in the NEW state (just created but not yet
+ connected) returns `False`.
+
+ Note: for some implementations `is_connected() != not is_closed()`.
+ The default implementation returns `not self.is_closed()`, which is
+ correct for connections that are never in the NEW (pre-TCP) state.
+ """
+ return not self.is_closed() # pragma: no cover
+
def is_available(self) -> bool:
"""
Return `True` if the connection is currently able to accept an
@@ -128,6 +140,16 @@
"""
raise NotImplementedError() # pragma: no cover
+ def can_multiplex(self) -> bool:
+ """
+ Return `True` if the connection can serve multiple requests
+ concurrently, such as an established HTTP/2 connection.
+
+ The default covers HTTP/1.1-style implementations, which serve a
+ single request at a time.
+ """
+ return False
+
def is_closed(self) -> bool:
"""
Return `True` if the connection has been closed.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_sync/socks_proxy.py
new/httpcore2-2.12.0/httpcore2/_sync/socks_proxy.py
--- old/httpcore2-2.5.0/httpcore2/_sync/socks_proxy.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_sync/socks_proxy.py 2020-02-02
01:00:00.000000000 +0100
@@ -240,7 +240,7 @@
trace.return_value = stream
# Upgrade the stream to SSL
- if self._remote_origin.scheme == b"https":
+ if self._remote_origin.scheme in (b"https", b"wss"):
ssl_context = default_ssl_context() if
self._ssl_context is None else self._ssl_context
alpn_protocols = ["http/1.1", "h2"] if self._http2
else ["http/1.1"]
ssl_context.set_alpn_protocols(alpn_protocols)
@@ -288,6 +288,9 @@
if self._connection is not None:
self._connection.close()
+ def is_connected(self) -> bool:
+ return self._connection is not None and self._connection.is_connected()
+
def is_available(self) -> bool:
if self._connection is None: # pragma: no cover
# If HTTP/2 support is enabled, and the resulting connection could
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/httpcore2/_synchronization.py
new/httpcore2-2.12.0/httpcore2/_synchronization.py
--- old/httpcore2-2.5.0/httpcore2/_synchronization.py 2020-02-02
01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/httpcore2/_synchronization.py 2020-02-02
01:00:00.000000000 +0100
@@ -18,13 +18,16 @@
except ImportError: # pragma: no cover
anyio = None # type: ignore
+try:
+ import sniffio
+except ImportError: # pragma: no cover
+ sniffio = None # type: ignore
+
def current_async_library() -> str:
# Determine if we're running under trio or asyncio.
# See https://sniffio.readthedocs.io/en/latest/
- try:
- import sniffio
- except ImportError: # pragma: no cover
+ if sniffio is None: # pragma: no cover
environment = "asyncio"
else:
environment = sniffio.current_async_library()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore2-2.5.0/pyproject.toml
new/httpcore2-2.12.0/pyproject.toml
--- old/httpcore2-2.5.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
+++ new/httpcore2-2.12.0/pyproject.toml 2020-02-02 01:00:00.000000000 +0100
@@ -2,7 +2,7 @@
requires = [
"hatchling",
"hatch-fancy-pypi-readme",
- "uv-dynamic-versioning>=0.8.0",
+ "uv-dynamic-versioning>=0.14.0",
]
build-backend = "hatchling.build"
@@ -41,6 +41,7 @@
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
"Topic :: Internet :: WWW/HTTP",
]
dependencies = ["truststore>=0.10", "h11>=0.16"]
@@ -48,7 +49,7 @@
[project.optional-dependencies]
http2 = ["h2>=3,<5"]
socks = ["socksio==1.*"]
-trio = ["trio>=0.22.0,<1.0"]
+trio = ["trio>=0.33.0,<1.0"]
asyncio = ["anyio>=4.5.0,<5.0"]
[project.urls]