Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-httpcore for openSUSE:Factory
checked in at 2023-10-26 17:11:47
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-httpcore (Old)
and /work/SRC/openSUSE:Factory/.python-httpcore.new.24901 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-httpcore"
Thu Oct 26 17:11:47 2023 rev:12 rq:1120297 version:0.18.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-httpcore/python-httpcore.changes
2023-09-12 21:02:33.671486062 +0200
+++
/work/SRC/openSUSE:Factory/.python-httpcore.new.24901/python-httpcore.changes
2023-10-26 17:11:50.078574000 +0200
@@ -1,0 +2,14 @@
+Wed Oct 25 11:30:12 UTC 2023 - Matej Cepl <[email protected]>
+
+- Update to 0.18.0:
+ - Add support for HTTPS proxies.
+ - Handle sni_hostname extension with SOCKS proxy.
+ - Change the type of Extensions from Mapping[Str, Any] to
+ MutableMapping[Str, Any].
+ - Handle HTTP/1.1 half-closed connections gracefully.
+ - Drop Python 3.7 support.
+- Update httpcore-allow-deprecationwarnings-test.patch
+- Skip failing tests test_ssl_request and test_extra_info
+ (gh#encode/httpcore!832)
+
+-------------------------------------------------------------------
Old:
----
httpcore-0.17.3.tar.gz
New:
----
httpcore-0.18.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-httpcore.spec ++++++
--- /var/tmp/diff_new_pack.JP9gi1/_old 2023-10-26 17:11:52.234653186 +0200
+++ /var/tmp/diff_new_pack.JP9gi1/_new 2023-10-26 17:11:52.234653186 +0200
@@ -27,7 +27,7 @@
%{?sle15_python_module_pythons}
Name: python-httpcore%{psuffix}
-Version: 0.17.3
+Version: 0.18.0
Release: 0
Summary: Minimal low-level Python HTTP client
License: BSD-3-Clause
@@ -36,7 +36,10 @@
# PATCH-FIX-UPSTREAM httpcore-allow-deprecationwarnings-test.patch
gh#encode/httpcore#511, gh#agronholm/anyio#470
Patch1: httpcore-allow-deprecationwarnings-test.patch
BuildRequires: %{python_module base >= 3.7}
-BuildRequires: %{python_module setuptools}
+BuildRequires: %{python_module hatch-fancy-pypi-readme}
+BuildRequires: %{python_module hatchling}
+BuildRequires: %{python_module pip}
+BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
Requires: python-certifi
@@ -68,10 +71,10 @@
%if !%{with test}
%build
-%python_build
+%pyproject_wheel
%install
-%python_install
+%pyproject_install
%python_expand %fdupes %{buildroot}%{$python_sitelib}
%endif
@@ -81,6 +84,8 @@
donttest="socks5"
# gh#encode/httpcore#622
donttest+=" or test_request_with_content"
+# gh#encode/httpcore!832
+donttest+=" or test_ssl_request or test_extra_info"
%pytest -rsfE --asyncio-mode=strict -p no:unraisableexception -k "not
($donttest)"
%endif
++++++ httpcore-0.17.3.tar.gz -> httpcore-0.18.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/.github/ISSUE_TEMPLATE/1-issue.md
new/httpcore-0.18.0/.github/ISSUE_TEMPLATE/1-issue.md
--- old/httpcore-0.17.3/.github/ISSUE_TEMPLATE/1-issue.md 1970-01-01
01:00:00.000000000 +0100
+++ new/httpcore-0.18.0/.github/ISSUE_TEMPLATE/1-issue.md 2023-09-08
15:37:47.000000000 +0200
@@ -0,0 +1,16 @@
+---
+name: Issue
+about: Please only raise an issue if you've been advised to do so after
discussion. Thanks! ð
+---
+
+The starting point for issues should usually be a discussion...
+
+https://github.com/encode/httpcore/discussions
+
+Possible bugs may be raised as a "Potential Issue" discussion, feature
requests may be raised as an "Ideas" discussion. We can then determine if the
discussion needs to be escalated into an "Issue" or not.
+
+This will help us ensure that the "Issues" list properly reflects ongoing or
needed work on the project.
+
+---
+
+- [ ] Initially raised as discussion #...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/.github/ISSUE_TEMPLATE/config.yml
new/httpcore-0.18.0/.github/ISSUE_TEMPLATE/config.yml
--- old/httpcore-0.17.3/.github/ISSUE_TEMPLATE/config.yml 1970-01-01
01:00:00.000000000 +0100
+++ new/httpcore-0.18.0/.github/ISSUE_TEMPLATE/config.yml 2023-09-08
15:37:47.000000000 +0200
@@ -0,0 +1,11 @@
+# Ref:
https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
+blank_issues_enabled: false
+contact_links:
+- name: Discussions
+ url: https://github.com/encode/httpcore/discussions
+ about: >
+ The "Discussions" forum is where you want to start. ð
+- name: Chat
+ url: https://gitter.im/encode/community
+ about: >
+ Our community chat forum.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/.github/PULL_REQUEST_TEMPLATE.md
new/httpcore-0.18.0/.github/PULL_REQUEST_TEMPLATE.md
--- old/httpcore-0.17.3/.github/PULL_REQUEST_TEMPLATE.md 1970-01-01
01:00:00.000000000 +0100
+++ new/httpcore-0.18.0/.github/PULL_REQUEST_TEMPLATE.md 2023-09-08
15:37:47.000000000 +0200
@@ -0,0 +1,12 @@
+<!-- Thanks for contributing to HTTP Core! ð
+Given this is a project maintained by volunteers, please read this template to
not waste your time, or ours! ð -->
+
+# Summary
+
+<!-- Write a small summary about what is happening here. -->
+
+# Checklist
+
+- [ ] I understand that this PR may be closed in case there was no previous
discussion. (This doesn't apply to typos!)
+- [ ] I've added a test for each change that was introduced, and I tried as
much as possible to make a single atomic change.
+- [ ] I've updated the documentation accordingly.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/.github/workflows/publish.yml
new/httpcore-0.18.0/.github/workflows/publish.yml
--- old/httpcore-0.17.3/.github/workflows/publish.yml 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/.github/workflows/publish.yml 2023-09-08
15:37:47.000000000 +0200
@@ -17,7 +17,7 @@
- uses: "actions/checkout@v3"
- uses: "actions/setup-python@v4"
with:
- python-version: 3.7
+ python-version: 3.8
- name: "Install dependencies"
run: "scripts/install"
- name: "Build package & docs"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/.github/workflows/test-suite.yml
new/httpcore-0.18.0/.github/workflows/test-suite.yml
--- old/httpcore-0.17.3/.github/workflows/test-suite.yml 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/.github/workflows/test-suite.yml 2023-09-08
15:37:47.000000000 +0200
@@ -14,7 +14,7 @@
strategy:
matrix:
- python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: "actions/checkout@v3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/CHANGELOG.md
new/httpcore-0.18.0/CHANGELOG.md
--- old/httpcore-0.17.3/CHANGELOG.md 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/CHANGELOG.md 2023-09-08 15:37:47.000000000 +0200
@@ -4,12 +4,21 @@
The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/).
-## 0.17.3 (5th July 2023)
+## 0.18.0 (September 8th, 2023)
+
+- Add support for HTTPS proxies. (#745, #786)
+- Drop Python 3.7 support. (#727)
+- Handle `sni_hostname` extension with SOCKS proxy. (#774)
+- Handle HTTP/1.1 half-closed connections gracefully. (#641)
+- Change the type of `Extensions` from `Mapping[Str, Any]` to
`MutableMapping[Str, Any]`. (#762)
+
+## 0.17.3 (July 5th, 2023)
- Support async cancellations, ensuring that the connection pool is left in a
clean state when cancellations occur. (#726)
- The networking backend interface has [been added to the public
API](https://www.encode.io/httpcore/network-backends). Some classes which were
previously private implementation detail are now part of the top-level public
API. (#699)
- Graceful handling of HTTP/2 GoAway frames, with requests being transparently
retried on a new connection. (#730)
- Add exceptions when a synchronous `trace callback` is passed to an
asynchronous request or an asynchronous `trace callback` is passed to a
synchronous request. (#717)
+- Drop Python 3.7 support. (#727)
## 0.17.2 (May 23th, 2023)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/MANIFEST.in
new/httpcore-0.18.0/MANIFEST.in
--- old/httpcore-0.17.3/MANIFEST.in 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/MANIFEST.in 1970-01-01 01:00:00.000000000 +0100
@@ -1,4 +0,0 @@
-include README.md
-include CHANGELOG.md
-include LICENSE.md
-include httpcore/py.typed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/README.md
new/httpcore-0.18.0/README.md
--- old/httpcore-0.17.3/README.md 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/README.md 2023-09-08 15:37:47.000000000 +0200
@@ -25,7 +25,7 @@
## Requirements
-Python 3.7+
+Python 3.8+
## Installation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/docs/index.md
new/httpcore-0.18.0/docs/index.md
--- old/httpcore-0.17.3/docs/index.md 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/docs/index.md 2023-09-08 15:37:47.000000000 +0200
@@ -25,7 +25,7 @@
## Requirements
-Python 3.7+
+Python 3.8+
## Installation
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/docs/proxies.md
new/httpcore-0.18.0/docs/proxies.md
--- old/httpcore-0.17.3/docs/proxies.md 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/docs/proxies.md 2023-09-08 15:37:47.000000000 +0200
@@ -51,10 +51,33 @@
)
```
-## Proxy SSL and HTTP Versions
+## Proxy SSL
-Proxy support currently only allows for HTTP/1.1 connections to the proxy,
-and does not currently support SSL proxy connections, which require
HTTPS-in-HTTPS,
+The `httpcore` package also supports HTTPS proxies for http and https
destinations.
+
+HTTPS proxies can be used in the same way that HTTP proxies are.
+
+```python
+proxy = httpcore.HTTPProxy(proxy_url="https://127.0.0.1:8080/")
+```
+
+Also, when using HTTPS proxies, you may need to configure the SSL context,
which you can do with the `proxy_ssl_context` argument.
+
+```python
+import ssl
+import httpcore
+
+proxy_ssl_context = ssl.create_default_context()
+proxy_ssl_context.check_hostname = False
+
+proxy = httpcore.HTTPProxy('https://127.0.0.1:8080/',
proxy_ssl_context=proxy_ssl_context)
+```
+
+It is important to note that the `ssl_context` argument is always used for the
remote connection, and the `proxy_ssl_context` argument is always used for the
proxy connection.
+
+## HTTP Versions
+
+If you use proxies, keep in mind that the `httpcore` package only supports
proxies to HTTP/1.1 servers.
## SOCKS proxy support
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/__init__.py
new/httpcore-0.18.0/httpcore/__init__.py
--- old/httpcore-0.17.3/httpcore/__init__.py 2023-07-04 12:34:39.000000000
+0200
+++ new/httpcore-0.18.0/httpcore/__init__.py 2023-09-08 15:37:47.000000000
+0200
@@ -130,7 +130,7 @@
"WriteError",
]
-__version__ = "0.17.3"
+__version__ = "0.18.0"
__locals = locals()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_async/connection.py
new/httpcore-0.18.0/httpcore/_async/connection.py
--- old/httpcore-0.17.3/httpcore/_async/connection.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_async/connection.py 2023-09-08
15:37:47.000000000 +0200
@@ -21,9 +21,16 @@
def exponential_backoff(factor: float) -> Iterator[float]:
+ """
+ Generate a geometric sequence that has a ratio of 2 and starts with 0.
+
+ For example:
+ - `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...`
+ - `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...`
+ """
yield 0
- for n in itertools.count(2):
- yield factor * (2 ** (n - 2))
+ for n in itertools.count():
+ yield factor * 2**n
class AsyncHTTPConnection(AsyncConnectionInterface):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_async/http11.py
new/httpcore-0.18.0/httpcore/_async/http11.py
--- old/httpcore-0.17.3/httpcore/_async/http11.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_async/http11.py 2023-09-08
15:37:47.000000000 +0200
@@ -20,6 +20,7 @@
ConnectionNotAvailable,
LocalProtocolError,
RemoteProtocolError,
+ WriteError,
map_exceptions,
)
from .._models import Origin, Request, Response
@@ -84,10 +85,21 @@
try:
kwargs = {"request": request}
- async with Trace("send_request_headers", logger, request, kwargs)
as trace:
- await self._send_request_headers(**kwargs)
- async with Trace("send_request_body", logger, request, kwargs) as
trace:
- await self._send_request_body(**kwargs)
+ try:
+ async with Trace(
+ "send_request_headers", logger, request, kwargs
+ ) as trace:
+ await self._send_request_headers(**kwargs)
+ async with Trace("send_request_body", logger, request, kwargs)
as trace:
+ await self._send_request_body(**kwargs)
+ except WriteError:
+ # If we get a write error while we're writing the request,
+ # then we supress this error and move on to attempting to
+ # read the response. Servers can sometimes close the request
+ # pre-emptively and then respond with a well formed HTTP
+ # error response.
+ pass
+
async with Trace(
"receive_response_headers", logger, request, kwargs
) as trace:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_async/http_proxy.py
new/httpcore-0.18.0/httpcore/_async/http_proxy.py
--- old/httpcore-0.17.3/httpcore/_async/http_proxy.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_async/http_proxy.py 2023-09-08
15:37:47.000000000 +0200
@@ -64,6 +64,7 @@
proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] =
None,
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
ssl_context: Optional[ssl.SSLContext] = None,
+ proxy_ssl_context: Optional[ssl.SSLContext] = None,
max_connections: Optional[int] = 10,
max_keepalive_connections: Optional[int] = None,
keepalive_expiry: Optional[float] = None,
@@ -88,6 +89,7 @@
ssl_context: An SSL context to use for verifying connections.
If not specified, the default `httpcore.default_ssl_context()`
will be used.
+ proxy_ssl_context: The same as `ssl_context`, but for a proxy
server rather than a remote origin.
max_connections: The maximum number of concurrent HTTP connections
that
the pool should allow. Any attempt to send a request on a pool
that
would exceed this amount will block until a connection is
available.
@@ -122,8 +124,17 @@
uds=uds,
socket_options=socket_options,
)
- self._ssl_context = ssl_context
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
+ if (
+ self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
+ ): # pragma: no cover
+ raise RuntimeError(
+ "The `proxy_ssl_context` argument is not allowed for the http
scheme"
+ )
+
+ self._ssl_context = ssl_context
+ self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers,
name="proxy_headers")
if proxy_auth is not None:
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
@@ -141,12 +152,14 @@
remote_origin=origin,
keepalive_expiry=self._keepalive_expiry,
network_backend=self._network_backend,
+ proxy_ssl_context=self._proxy_ssl_context,
)
return AsyncTunnelHTTPConnection(
proxy_origin=self._proxy_url.origin,
proxy_headers=self._proxy_headers,
remote_origin=origin,
ssl_context=self._ssl_context,
+ proxy_ssl_context=self._proxy_ssl_context,
keepalive_expiry=self._keepalive_expiry,
http1=self._http1,
http2=self._http2,
@@ -163,12 +176,14 @@
keepalive_expiry: Optional[float] = None,
network_backend: Optional[AsyncNetworkBackend] = None,
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
+ proxy_ssl_context: Optional[ssl.SSLContext] = None,
) -> None:
self._connection = AsyncHTTPConnection(
origin=proxy_origin,
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
+ ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._proxy_headers = enforce_headers(proxy_headers,
name="proxy_headers")
@@ -222,6 +237,7 @@
proxy_origin: Origin,
remote_origin: Origin,
ssl_context: Optional[ssl.SSLContext] = None,
+ proxy_ssl_context: Optional[ssl.SSLContext] = None,
proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
keepalive_expiry: Optional[float] = None,
http1: bool = True,
@@ -234,10 +250,12 @@
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
+ ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._remote_origin = remote_origin
self._ssl_context = ssl_context
+ self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers,
name="proxy_headers")
self._keepalive_expiry = keepalive_expiry
self._http1 = http1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_async/socks_proxy.py
new/httpcore-0.18.0/httpcore/_async/socks_proxy.py
--- old/httpcore-0.17.3/httpcore/_async/socks_proxy.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_async/socks_proxy.py 2023-09-08
15:37:47.000000000 +0200
@@ -216,6 +216,7 @@
async def handle_async_request(self, request: Request) -> Response:
timeouts = request.extensions.get("timeout", {})
+ sni_hostname = request.extensions.get("sni_hostname", None)
timeout = timeouts.get("connect", None)
async with self._connect_lock:
@@ -258,7 +259,8 @@
kwargs = {
"ssl_context": ssl_context,
- "server_hostname":
self._remote_origin.host.decode("ascii"),
+ "server_hostname": sni_hostname
+ or self._remote_origin.host.decode("ascii"),
"timeout": timeout,
}
async with Trace("start_tls", logger, request, kwargs)
as trace:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_backends/sync.py
new/httpcore-0.18.0/httpcore/_backends/sync.py
--- old/httpcore-0.17.3/httpcore/_backends/sync.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_backends/sync.py 2023-09-08
15:37:47.000000000 +0200
@@ -2,6 +2,7 @@
import ssl
import sys
import typing
+from functools import partial
from .._exceptions import (
ConnectError,
@@ -17,6 +18,103 @@
from .base import SOCKET_OPTION, NetworkBackend, NetworkStream
+class TLSinTLSStream(NetworkStream): # pragma: no cover
+ """
+ Because the standard `SSLContext.wrap_socket` method does
+ not work for `SSLSocket` objects, we need this class
+ to implement TLS stream using an underlying `SSLObject`
+ instance in order to support TLS on top of TLS.
+ """
+
+ # Defined in RFC 8449
+ TLS_RECORD_SIZE = 16384
+
+ def __init__(
+ self,
+ sock: socket.socket,
+ ssl_context: ssl.SSLContext,
+ server_hostname: typing.Optional[str] = None,
+ timeout: typing.Optional[float] = None,
+ ):
+ self._sock = sock
+ self._incoming = ssl.MemoryBIO()
+ self._outgoing = ssl.MemoryBIO()
+
+ self.ssl_obj = ssl_context.wrap_bio(
+ incoming=self._incoming,
+ outgoing=self._outgoing,
+ server_hostname=server_hostname,
+ )
+
+ self._sock.settimeout(timeout)
+ self._perform_io(self.ssl_obj.do_handshake)
+
+ def _perform_io(
+ self,
+ func: typing.Callable[..., typing.Any],
+ ) -> typing.Any:
+ ret = None
+
+ while True:
+ errno = None
+ try:
+ ret = func()
+ except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e:
+ errno = e.errno
+
+ self._sock.sendall(self._outgoing.read())
+
+ if errno == ssl.SSL_ERROR_WANT_READ:
+ buf = self._sock.recv(self.TLS_RECORD_SIZE)
+
+ if buf:
+ self._incoming.write(buf)
+ else:
+ self._incoming.write_eof()
+ if errno is None:
+ return ret
+
+ def read(self, max_bytes: int, timeout: typing.Optional[float] = None) ->
bytes:
+ exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError:
ReadError}
+ with map_exceptions(exc_map):
+ self._sock.settimeout(timeout)
+ return typing.cast(
+ bytes, self._perform_io(partial(self.ssl_obj.read, max_bytes))
+ )
+
+ def write(self, buffer: bytes, timeout: typing.Optional[float] = None) ->
None:
+ exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError:
WriteError}
+ with map_exceptions(exc_map):
+ self._sock.settimeout(timeout)
+ while buffer:
+ nsent = self._perform_io(partial(self.ssl_obj.write, buffer))
+ buffer = buffer[nsent:]
+
+ def close(self) -> None:
+ self._sock.close()
+
+ def start_tls(
+ self,
+ ssl_context: ssl.SSLContext,
+ server_hostname: typing.Optional[str] = None,
+ timeout: typing.Optional[float] = None,
+ ) -> "NetworkStream":
+ raise NotImplementedError()
+
+ def get_extra_info(self, info: str) -> typing.Any:
+ if info == "ssl_object":
+ return self.ssl_obj
+ if info == "client_addr":
+ return self._sock.getsockname()
+ if info == "server_addr":
+ return self._sock.getpeername()
+ if info == "socket":
+ return self._sock
+ if info == "is_readable":
+ return is_socket_readable(self._sock)
+ return None
+
+
class SyncStream(NetworkStream):
def __init__(self, sock: socket.socket) -> None:
self._sock = sock
@@ -47,16 +145,30 @@
server_hostname: typing.Optional[str] = None,
timeout: typing.Optional[float] = None,
) -> NetworkStream:
+ if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
+ raise RuntimeError(
+ "Attempted to add a TLS layer on top of the existing "
+ "TLS stream, which is not supported by httpcore package"
+ )
+
exc_map: ExceptionMapping = {
socket.timeout: ConnectTimeout,
OSError: ConnectError,
}
with map_exceptions(exc_map):
try:
- self._sock.settimeout(timeout)
- sock = ssl_context.wrap_socket(
- self._sock, server_hostname=server_hostname
- )
+ if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
+ # If the underlying socket has already been upgraded
+ # to the TLS layer (i.e. is an instance of SSLSocket),
+ # we need some additional smarts to support TLS-in-TLS.
+ return TLSinTLSStream(
+ self._sock, ssl_context, server_hostname, timeout
+ )
+ else:
+ self._sock.settimeout(timeout)
+ sock = ssl_context.wrap_socket(
+ self._sock, server_hostname=server_hostname
+ )
except Exception as exc: # pragma: nocover
self.close()
raise exc
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_models.py
new/httpcore-0.18.0/httpcore/_models.py
--- old/httpcore-0.17.3/httpcore/_models.py 2023-07-04 12:34:39.000000000
+0200
+++ new/httpcore-0.18.0/httpcore/_models.py 2023-09-08 15:37:47.000000000
+0200
@@ -6,6 +6,7 @@
Iterator,
List,
Mapping,
+ MutableMapping,
Optional,
Sequence,
Tuple,
@@ -20,7 +21,7 @@
HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]]
HeaderTypes = Union[HeadersAsSequence, HeadersAsMapping, None]
-Extensions = Mapping[str, Any]
+Extensions = MutableMapping[str, Any]
def enforce_bytes(value: Union[bytes, str], *, name: str) -> bytes:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_sync/connection.py
new/httpcore-0.18.0/httpcore/_sync/connection.py
--- old/httpcore-0.17.3/httpcore/_sync/connection.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_sync/connection.py 2023-09-08
15:37:47.000000000 +0200
@@ -21,9 +21,16 @@
def exponential_backoff(factor: float) -> Iterator[float]:
+ """
+ Generate a geometric sequence that has a ratio of 2 and starts with 0.
+
+ For example:
+ - `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...`
+ - `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...`
+ """
yield 0
- for n in itertools.count(2):
- yield factor * (2 ** (n - 2))
+ for n in itertools.count():
+ yield factor * 2**n
class HTTPConnection(ConnectionInterface):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_sync/http11.py
new/httpcore-0.18.0/httpcore/_sync/http11.py
--- old/httpcore-0.17.3/httpcore/_sync/http11.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_sync/http11.py 2023-09-08
15:37:47.000000000 +0200
@@ -20,6 +20,7 @@
ConnectionNotAvailable,
LocalProtocolError,
RemoteProtocolError,
+ WriteError,
map_exceptions,
)
from .._models import Origin, Request, Response
@@ -84,10 +85,21 @@
try:
kwargs = {"request": request}
- with Trace("send_request_headers", logger, request, kwargs) as
trace:
- self._send_request_headers(**kwargs)
- with Trace("send_request_body", logger, request, kwargs) as trace:
- self._send_request_body(**kwargs)
+ try:
+ with Trace(
+ "send_request_headers", logger, request, kwargs
+ ) as trace:
+ self._send_request_headers(**kwargs)
+ with Trace("send_request_body", logger, request, kwargs) as
trace:
+ self._send_request_body(**kwargs)
+ except WriteError:
+ # If we get a write error while we're writing the request,
+ # then we supress this error and move on to attempting to
+ # read the response. Servers can sometimes close the request
+ # pre-emptively and then respond with a well formed HTTP
+ # error response.
+ pass
+
with Trace(
"receive_response_headers", logger, request, kwargs
) as trace:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_sync/http_proxy.py
new/httpcore-0.18.0/httpcore/_sync/http_proxy.py
--- old/httpcore-0.17.3/httpcore/_sync/http_proxy.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_sync/http_proxy.py 2023-09-08
15:37:47.000000000 +0200
@@ -64,6 +64,7 @@
proxy_auth: Optional[Tuple[Union[bytes, str], Union[bytes, str]]] =
None,
proxy_headers: Union[HeadersAsMapping, HeadersAsSequence, None] = None,
ssl_context: Optional[ssl.SSLContext] = None,
+ proxy_ssl_context: Optional[ssl.SSLContext] = None,
max_connections: Optional[int] = 10,
max_keepalive_connections: Optional[int] = None,
keepalive_expiry: Optional[float] = None,
@@ -88,6 +89,7 @@
ssl_context: An SSL context to use for verifying connections.
If not specified, the default `httpcore.default_ssl_context()`
will be used.
+ proxy_ssl_context: The same as `ssl_context`, but for a proxy
server rather than a remote origin.
max_connections: The maximum number of concurrent HTTP connections
that
the pool should allow. Any attempt to send a request on a pool
that
would exceed this amount will block until a connection is
available.
@@ -122,8 +124,17 @@
uds=uds,
socket_options=socket_options,
)
- self._ssl_context = ssl_context
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
+ if (
+ self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
+ ): # pragma: no cover
+ raise RuntimeError(
+ "The `proxy_ssl_context` argument is not allowed for the http
scheme"
+ )
+
+ self._ssl_context = ssl_context
+ self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers,
name="proxy_headers")
if proxy_auth is not None:
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
@@ -141,12 +152,14 @@
remote_origin=origin,
keepalive_expiry=self._keepalive_expiry,
network_backend=self._network_backend,
+ proxy_ssl_context=self._proxy_ssl_context,
)
return TunnelHTTPConnection(
proxy_origin=self._proxy_url.origin,
proxy_headers=self._proxy_headers,
remote_origin=origin,
ssl_context=self._ssl_context,
+ proxy_ssl_context=self._proxy_ssl_context,
keepalive_expiry=self._keepalive_expiry,
http1=self._http1,
http2=self._http2,
@@ -163,12 +176,14 @@
keepalive_expiry: Optional[float] = None,
network_backend: Optional[NetworkBackend] = None,
socket_options: Optional[Iterable[SOCKET_OPTION]] = None,
+ proxy_ssl_context: Optional[ssl.SSLContext] = None,
) -> None:
self._connection = HTTPConnection(
origin=proxy_origin,
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
+ ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._proxy_headers = enforce_headers(proxy_headers,
name="proxy_headers")
@@ -222,6 +237,7 @@
proxy_origin: Origin,
remote_origin: Origin,
ssl_context: Optional[ssl.SSLContext] = None,
+ proxy_ssl_context: Optional[ssl.SSLContext] = None,
proxy_headers: Optional[Sequence[Tuple[bytes, bytes]]] = None,
keepalive_expiry: Optional[float] = None,
http1: bool = True,
@@ -234,10 +250,12 @@
keepalive_expiry=keepalive_expiry,
network_backend=network_backend,
socket_options=socket_options,
+ ssl_context=proxy_ssl_context,
)
self._proxy_origin = proxy_origin
self._remote_origin = remote_origin
self._ssl_context = ssl_context
+ self._proxy_ssl_context = proxy_ssl_context
self._proxy_headers = enforce_headers(proxy_headers,
name="proxy_headers")
self._keepalive_expiry = keepalive_expiry
self._http1 = http1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/httpcore/_sync/socks_proxy.py
new/httpcore-0.18.0/httpcore/_sync/socks_proxy.py
--- old/httpcore-0.17.3/httpcore/_sync/socks_proxy.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/httpcore/_sync/socks_proxy.py 2023-09-08
15:37:47.000000000 +0200
@@ -216,6 +216,7 @@
def handle_request(self, request: Request) -> Response:
timeouts = request.extensions.get("timeout", {})
+ sni_hostname = request.extensions.get("sni_hostname", None)
timeout = timeouts.get("connect", None)
with self._connect_lock:
@@ -258,7 +259,8 @@
kwargs = {
"ssl_context": ssl_context,
- "server_hostname":
self._remote_origin.host.decode("ascii"),
+ "server_hostname": sni_hostname
+ or self._remote_origin.host.decode("ascii"),
"timeout": timeout,
}
with Trace("start_tls", logger, request, kwargs) as
trace:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/pyproject.toml
new/httpcore-0.18.0/pyproject.toml
--- old/httpcore-0.17.3/pyproject.toml 1970-01-01 01:00:00.000000000 +0100
+++ new/httpcore-0.18.0/pyproject.toml 2023-09-08 15:37:47.000000000 +0200
@@ -0,0 +1,112 @@
+[build-system]
+requires = ["hatchling", "hatch-fancy-pypi-readme"]
+build-backend = "hatchling.build"
+
+[project]
+name = "httpcore"
+dynamic = ["readme", "version"]
+description = "A minimal low-level HTTP client."
+license = "BSD-3-Clause"
+requires-python = ">=3.8"
+authors = [
+ { name = "Tom Christie", email = "[email protected]" },
+]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Environment :: Web Environment",
+ "Framework :: AsyncIO",
+ "Framework :: Trio",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Topic :: Internet :: WWW/HTTP",
+]
+dependencies = [
+ "anyio>=3.0,<5.0",
+ "certifi",
+ "h11>=0.13,<0.15",
+ "sniffio==1.*",
+]
+
+[project.optional-dependencies]
+http2 = [
+ "h2>=3,<5",
+]
+socks = [
+ "socksio==1.*",
+]
+
+[project.urls]
+Documentation = "https://www.encode.io/httpcore"
+Homepage = "https://www.encode.io/httpcore/"
+Source = "https://github.com/encode/httpcore"
+
+[tool.hatch.version]
+path = "httpcore/__init__.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/httpcore",
+ "/CHANGELOG.md",
+ "/README.md",
+]
+
+[tool.hatch.metadata.hooks.fancy-pypi-readme]
+content-type = "text/markdown"
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+path = "README.md"
+
+[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
+path = "CHANGELOG.md"
+
+[tool.mypy]
+strict = true
+show_error_codes = true
+
+[[tool.mypy.overrides]]
+module = "tests.*"
+disallow_untyped_defs = false
+check_untyped_defs = true
+
+[[tool.mypy.overrides]]
+module = "h2.*"
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = "hpack.*"
+ignore_missing_imports = true
+
+[tool.pytest.ini_options]
+addopts = ["-rxXs", "--strict-config", "--strict-markers"]
+markers = ["copied_from(source, changes=None): mark test as copied from
somewhere else, along with a description of changes made to accodomate e.g. our
test setup"]
+filterwarnings = ["error"]
+
+[tool.coverage.run]
+omit = [
+ "venv/*",
+ "httpcore/_sync/*"
+]
+include = ["httpcore/*", "tests/*"]
+
+[tool.ruff]
+exclude = [
+ "httpcore/_sync",
+ "tests/_sync",
+]
+line-length = 120
+select = [
+ "E",
+ "F",
+ "W",
+ "I"
+]
+
+[tool.ruff.isort]
+combine-as-imports = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/requirements.txt
new/httpcore-0.18.0/requirements.txt
--- old/httpcore-0.17.3/requirements.txt 2023-07-04 12:34:39.000000000
+0200
+++ new/httpcore-0.18.0/requirements.txt 2023-09-08 15:37:47.000000000
+0200
@@ -5,25 +5,22 @@
# Docs
mkdocs==1.4.2
-mkdocs-autorefs==0.3.1
+mkdocs-autorefs==0.5.0
mkdocs-material==9.1.15
mkdocs-material-extensions==1.1.1
mkdocstrings[python-legacy]==0.22.0
jinja2==3.1.2
# Packaging
+build==0.10.0
twine
-wheel
# Tests & Linting
-anyio==3.6.2
-autoflake==1.7.7
-black==23.3.0
-coverage==7.2.7
-flake8==3.9.2 # See: https://github.com/PyCQA/flake8/pull/1438
-isort==5.11.4
-importlib-metadata==4.13.0
-mypy==1.2.0
+anyio==3.7.1
+black==23.7.0
+coverage[toml]==7.3.0
+ruff==0.0.277
+mypy==1.5.1
trio-typing==0.8.0
types-certifi==2021.10.8.3
pytest==7.4.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/scripts/build
new/httpcore-0.18.0/scripts/build
--- old/httpcore-0.17.3/scripts/build 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/scripts/build 2023-09-08 15:37:47.000000000 +0200
@@ -10,6 +10,6 @@
set -x
-${PREFIX}python setup.py sdist bdist_wheel
+${PREFIX}python -m build
${PREFIX}twine check dist/*
${PREFIX}mkdocs build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/scripts/check
new/httpcore-0.18.0/scripts/check
--- old/httpcore-0.17.3/scripts/check 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/scripts/check 2023-09-08 15:37:47.000000000 +0200
@@ -8,8 +8,7 @@
set -x
-${PREFIX}isort --check --diff --project=httpcore $SOURCE_FILES
-${PREFIX}black --exclude '/(_sync|sync_tests)/' --check --diff
--target-version=py37 $SOURCE_FILES
-${PREFIX}flake8 $SOURCE_FILES
+${PREFIX}ruff check --show-source $SOURCE_FILES
+${PREFIX}black --exclude '/(_sync|sync_tests)/' --check --diff $SOURCE_FILES
${PREFIX}mypy $SOURCE_FILES
scripts/unasync --check
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/scripts/lint
new/httpcore-0.18.0/scripts/lint
--- old/httpcore-0.17.3/scripts/lint 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/scripts/lint 2023-09-08 15:37:47.000000000 +0200
@@ -8,9 +8,8 @@
set -x
-${PREFIX}autoflake --in-place --recursive --remove-all-unused-imports
$SOURCE_FILES
-${PREFIX}isort --project=httpcore $SOURCE_FILES
-${PREFIX}black --target-version=py37 --exclude '/(_sync|sync_tests)/'
$SOURCE_FILES
+${PREFIX}ruff --fix $SOURCE_FILES
+${PREFIX}black --exclude '/(_sync|sync_tests)/' $SOURCE_FILES
# Run unasync last because its `--check` mode is not aware of code formatters.
# (This means sync code isn't prettified, and that's mostly okay.)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/setup.cfg
new/httpcore-0.18.0/setup.cfg
--- old/httpcore-0.17.3/setup.cfg 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/setup.cfg 1970-01-01 01:00:00.000000000 +0100
@@ -1,36 +0,0 @@
-[flake8]
-ignore = W503, E203, B305
-max-line-length = 120
-exclude = httpcore/_sync,tests/_sync
-
-[mypy]
-strict = True
-show_error_codes = True
-
-[mypy-tests.*]
-disallow_untyped_defs = False
-check_untyped_defs = True
-
-[mypy-h2.*]
-ignore_missing_imports = True
-
-[mypy-hpack.*]
-ignore_missing_imports = True
-
-[tool:isort]
-profile = black
-combine_as_imports = True
-known_first_party = httpcore,tests
-known_third_party =
brotli,certifi,chardet,cryptography,h11,h2,hstspreload,pytest,rfc3986,setuptools,sniffio,trio,trustme,urllib3,uvicorn
-skip = httpcore/_sync/,tests/_sync
-
-[tool:pytest]
-addopts = -rxXs --strict-config --strict-markers
-markers =
- copied_from(source, changes=None): mark test as copied from somewhere else,
along with a description of changes made to accodomate e.g. our test setup
-filterwarnings =
- error
-
-[coverage:run]
-omit = venv/*, httpcore/_sync/*
-include = httpcore/*, tests/*
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/setup.py new/httpcore-0.18.0/setup.py
--- old/httpcore-0.17.3/setup.py 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/setup.py 1970-01-01 01:00:00.000000000 +0100
@@ -1,83 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-import re
-from pathlib import Path
-
-from setuptools import setup
-
-
-def get_version(package):
- """
- Return package version as listed in `__version__` in `init.py`.
- """
- version = Path(package, "__init__.py").read_text()
- return re.search("__version__ = ['\"]([^'\"]+)['\"]", version).group(1)
-
-
-def get_long_description():
- """
- Return the README.
- """
- long_description = ""
- with open("README.md", encoding="utf8") as f:
- long_description += f.read()
- long_description += "\n\n"
- with open("CHANGELOG.md", encoding="utf8") as f:
- long_description += f.read()
- return long_description
-
-
-def get_packages(package):
- """
- Return root package and all sub-packages.
- """
- return [str(path.parent) for path in Path(package).glob("**/__init__.py")]
-
-
-setup(
- name="httpcore",
- python_requires=">=3.7",
- version=get_version("httpcore"),
- url="https://github.com/encode/httpcore",
- project_urls={
- "Documentation": "https://www.encode.io/httpcore",
- "Source": "https://github.com/encode/httpcore",
- },
- license="BSD",
- description="A minimal low-level HTTP client.",
- long_description=get_long_description(),
- long_description_content_type="text/markdown",
- author="Tom Christie",
- author_email="[email protected]",
- packages=get_packages("httpcore"),
- include_package_data=True,
- zip_safe=False,
- install_requires=[
- "h11>=0.13,<0.15",
- "sniffio==1.*",
- "anyio>=3.0,<5.0",
- "certifi",
- ],
- extras_require={
- "http2": ["h2>=3,<5"],
- "socks": ["socksio==1.*"]
- },
- classifiers=[
- "Development Status :: 3 - Alpha",
- "Environment :: Web Environment",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: BSD License",
- "Operating System :: OS Independent",
- "Topic :: Internet :: WWW/HTTP",
- "Framework :: AsyncIO",
- "Framework :: Trio",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3 :: Only",
- ],
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/tests/_async/test_connection.py
new/httpcore-0.18.0/tests/_async/test_connection.py
--- old/httpcore-0.17.3/tests/_async/test_connection.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/tests/_async/test_connection.py 2023-09-08
15:37:47.000000000 +0200
@@ -9,10 +9,13 @@
SOCKET_OPTION,
AsyncHTTPConnection,
AsyncMockBackend,
+ AsyncMockStream,
AsyncNetworkStream,
ConnectError,
ConnectionNotAvailable,
Origin,
+ RemoteProtocolError,
+ WriteError,
)
@@ -83,7 +86,109 @@
await conn.request("GET", "https://example.com/")
[email protected]("ignore::pytest.PytestUnraisableExceptionWarning")
@pytest.mark.anyio
+async def test_write_error_with_response_sent():
+ """
+ If a server half-closes the connection while the client is sending
+ the request, it may still send a response. In this case the client
+ should successfully read and return the response.
+
+ See also the `test_write_error_without_response_sent` test above.
+ """
+
+ class ErrorOnRequestTooLargeStream(AsyncMockStream):
+ def __init__(self, buffer: typing.List[bytes], http2: bool = False) ->
None:
+ super().__init__(buffer, http2)
+ self.count = 0
+
+ async def write(
+ self, buffer: bytes, timeout: typing.Optional[float] = None
+ ) -> None:
+ self.count += len(buffer)
+
+ if self.count > 1_000_000:
+ raise WriteError()
+
+ class ErrorOnRequestTooLarge(AsyncMockBackend):
+ async def connect_tcp(
+ self,
+ host: str,
+ port: int,
+ timeout: typing.Optional[float] = None,
+ local_address: typing.Optional[str] = None,
+ socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] =
None,
+ ) -> AsyncMockStream:
+ return ErrorOnRequestTooLargeStream(list(self._buffer),
http2=self._http2)
+
+ origin = Origin(b"https", b"example.com", 443)
+ network_backend = ErrorOnRequestTooLarge(
+ [
+ b"HTTP/1.1 413 Payload Too Large\r\n",
+ b"Content-Type: plain/text\r\n",
+ b"Content-Length: 37\r\n",
+ b"\r\n",
+ b"Request body exceeded 1,000,000 bytes",
+ ]
+ )
+
+ async with AsyncHTTPConnection(
+ origin=origin, network_backend=network_backend, keepalive_expiry=5.0
+ ) as conn:
+ content = b"x" * 10_000_000
+ response = await conn.request("POST", "https://example.com/",
content=content)
+ assert response.status == 413
+ assert response.content == b"Request body exceeded 1,000,000 bytes"
+
+
[email protected]
[email protected]("ignore::pytest.PytestUnraisableExceptionWarning")
+async def test_write_error_without_response_sent():
+ """
+ If a server fully closes the connection while the client is sending
+ the request, then client should raise an error.
+
+ See also the `test_write_error_with_response_sent` test above.
+ """
+
+ class ErrorOnRequestTooLargeStream(AsyncMockStream):
+ def __init__(self, buffer: typing.List[bytes], http2: bool = False) ->
None:
+ super().__init__(buffer, http2)
+ self.count = 0
+
+ async def write(
+ self, buffer: bytes, timeout: typing.Optional[float] = None
+ ) -> None:
+ self.count += len(buffer)
+
+ if self.count > 1_000_000:
+ raise WriteError()
+
+ class ErrorOnRequestTooLarge(AsyncMockBackend):
+ async def connect_tcp(
+ self,
+ host: str,
+ port: int,
+ timeout: typing.Optional[float] = None,
+ local_address: typing.Optional[str] = None,
+ socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] =
None,
+ ) -> AsyncMockStream:
+ return ErrorOnRequestTooLargeStream(list(self._buffer),
http2=self._http2)
+
+ origin = Origin(b"https", b"example.com", 443)
+ network_backend = ErrorOnRequestTooLarge([])
+
+ async with AsyncHTTPConnection(
+ origin=origin, network_backend=network_backend, keepalive_expiry=5.0
+ ) as conn:
+ content = b"x" * 10_000_000
+ with pytest.raises(RemoteProtocolError) as exc_info:
+ await conn.request("POST", "https://example.com/", content=content)
+ assert str(exc_info.value) == "Server disconnected without sending a
response."
+
+
[email protected]
[email protected]("ignore::pytest.PytestUnraisableExceptionWarning")
async def test_http2_connection():
origin = Origin(b"https", b"example.com", 443)
network_backend = AsyncMockBackend(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/tests/_sync/test_connection.py
new/httpcore-0.18.0/tests/_sync/test_connection.py
--- old/httpcore-0.17.3/tests/_sync/test_connection.py 2023-07-04
12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/tests/_sync/test_connection.py 2023-09-08
15:37:47.000000000 +0200
@@ -9,10 +9,13 @@
SOCKET_OPTION,
HTTPConnection,
MockBackend,
+ MockStream,
NetworkStream,
ConnectError,
ConnectionNotAvailable,
Origin,
+ RemoteProtocolError,
+ WriteError,
)
@@ -83,7 +86,109 @@
conn.request("GET", "https://example.com/")
[email protected]("ignore::pytest.PytestUnraisableExceptionWarning")
+def test_write_error_with_response_sent():
+ """
+ If a server half-closes the connection while the client is sending
+ the request, it may still send a response. In this case the client
+ should successfully read and return the response.
+
+ See also the `test_write_error_without_response_sent` test above.
+ """
+
+ class ErrorOnRequestTooLargeStream(MockStream):
+ def __init__(self, buffer: typing.List[bytes], http2: bool = False) ->
None:
+ super().__init__(buffer, http2)
+ self.count = 0
+
+ def write(
+ self, buffer: bytes, timeout: typing.Optional[float] = None
+ ) -> None:
+ self.count += len(buffer)
+
+ if self.count > 1_000_000:
+ raise WriteError()
+
+ class ErrorOnRequestTooLarge(MockBackend):
+ def connect_tcp(
+ self,
+ host: str,
+ port: int,
+ timeout: typing.Optional[float] = None,
+ local_address: typing.Optional[str] = None,
+ socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] =
None,
+ ) -> MockStream:
+ return ErrorOnRequestTooLargeStream(list(self._buffer),
http2=self._http2)
+
+ origin = Origin(b"https", b"example.com", 443)
+ network_backend = ErrorOnRequestTooLarge(
+ [
+ b"HTTP/1.1 413 Payload Too Large\r\n",
+ b"Content-Type: plain/text\r\n",
+ b"Content-Length: 37\r\n",
+ b"\r\n",
+ b"Request body exceeded 1,000,000 bytes",
+ ]
+ )
+
+ with HTTPConnection(
+ origin=origin, network_backend=network_backend, keepalive_expiry=5.0
+ ) as conn:
+ content = b"x" * 10_000_000
+ response = conn.request("POST", "https://example.com/",
content=content)
+ assert response.status == 413
+ assert response.content == b"Request body exceeded 1,000,000 bytes"
+
+
+
[email protected]("ignore::pytest.PytestUnraisableExceptionWarning")
+def test_write_error_without_response_sent():
+ """
+ If a server fully closes the connection while the client is sending
+ the request, then client should raise an error.
+
+ See also the `test_write_error_with_response_sent` test above.
+ """
+
+ class ErrorOnRequestTooLargeStream(MockStream):
+ def __init__(self, buffer: typing.List[bytes], http2: bool = False) ->
None:
+ super().__init__(buffer, http2)
+ self.count = 0
+
+ def write(
+ self, buffer: bytes, timeout: typing.Optional[float] = None
+ ) -> None:
+ self.count += len(buffer)
+
+ if self.count > 1_000_000:
+ raise WriteError()
+
+ class ErrorOnRequestTooLarge(MockBackend):
+ def connect_tcp(
+ self,
+ host: str,
+ port: int,
+ timeout: typing.Optional[float] = None,
+ local_address: typing.Optional[str] = None,
+ socket_options: typing.Optional[typing.Iterable[SOCKET_OPTION]] =
None,
+ ) -> MockStream:
+ return ErrorOnRequestTooLargeStream(list(self._buffer),
http2=self._http2)
+
+ origin = Origin(b"https", b"example.com", 443)
+ network_backend = ErrorOnRequestTooLarge([])
+
+ with HTTPConnection(
+ origin=origin, network_backend=network_backend, keepalive_expiry=5.0
+ ) as conn:
+ content = b"x" * 10_000_000
+ with pytest.raises(RemoteProtocolError) as exc_info:
+ conn.request("POST", "https://example.com/", content=content)
+ assert str(exc_info.value) == "Server disconnected without sending a
response."
+
+
+
[email protected]("ignore::pytest.PytestUnraisableExceptionWarning")
def test_http2_connection():
origin = Origin(b"https", b"example.com", 443)
network_backend = MockBackend(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/httpcore-0.17.3/unasync.py
new/httpcore-0.18.0/unasync.py
--- old/httpcore-0.17.3/unasync.py 2023-07-04 12:34:39.000000000 +0200
+++ new/httpcore-0.18.0/unasync.py 2023-09-08 15:37:47.000000000 +0200
@@ -2,13 +2,12 @@
import os
import re
import sys
+from pprint import pprint
SUBS = [
('from .._backends.auto import AutoBackend', 'from .._backends.sync import
SyncBackend'),
('import trio as concurrency', 'from tests import concurrency'),
- ('AsyncByteStream', 'SyncByteStream'),
('AsyncIterator', 'Iterator'),
- ('AutoBackend', 'SyncBackend'),
('Async([A-Z][A-Za-z0-9_]*)', r'\2'),
('async def', 'def'),
('async with', 'with'),
@@ -16,8 +15,6 @@
('await ', ''),
('handle_async_request', 'handle_request'),
('aclose', 'close'),
- ('aclose_func', 'close_func'),
- ('aiterator', 'iterator'),
('aiter_stream', 'iter_stream'),
('aread', 'read'),
('asynccontextmanager', 'contextmanager'),
@@ -33,10 +30,14 @@
for regex, repl in SUBS
]
+USED_SUBS = set()
def unasync_line(line):
- for regex, repl in COMPILED_SUBS:
+ for index, (regex, repl) in enumerate(COMPILED_SUBS):
+ old_line = line
line = re.sub(regex, repl, line)
+ if old_line != line:
+ USED_SUBS.add(index)
return line
@@ -81,6 +82,13 @@
unasync_dir("httpcore/_async", "httpcore/_sync", check_only=check_only)
unasync_dir("tests/_async", "tests/_sync", check_only=check_only)
+ if len(USED_SUBS) != len(SUBS):
+ unused_subs = [SUBS[i] for i in range(len(SUBS)) if i not in USED_SUBS]
+
+ print("These patterns were not used:")
+ pprint(unused_subs)
+ exit(1)
+
if __name__ == '__main__':
main()
++++++ httpcore-allow-deprecationwarnings-test.patch ++++++
--- /var/tmp/diff_new_pack.JP9gi1/_old 2023-10-26 17:11:52.326656565 +0200
+++ /var/tmp/diff_new_pack.JP9gi1/_new 2023-10-26 17:11:52.330656712 +0200
@@ -1,18 +1,22 @@
-Index: httpcore-0.16.3/setup.cfg
-===================================================================
---- httpcore-0.16.3.orig/setup.cfg
-+++ httpcore-0.16.3/setup.cfg
-@@ -30,6 +30,12 @@ markers =
- copied_from(source, changes=None): mark test as copied from somewhere else,
along with a description of changes made to accodomate e.g. our test setup
- filterwarnings =
- error
-+ # requires anyio 4 with trio 0.22:
https://github.com/agronholm/anyio/issues/470
-+ ignore:trio.MultiError is deprecated
-+ # fixed by pytest-httpbin (2.0 not released yet):
https://github.com/encode/httpcore/pull/511
-+ ignore:unclosed <(socket\.socket|ssl\.SSLSocket) .*:ResourceWarning
-+ ignore:ssl\.wrap_socket\(\) is deprecated, use
SSLContext\.wrap_socket\(\):DeprecationWarning
-+ ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning
+---
+ pyproject.toml | 8 +++++++-
+ 1 file changed, 7 insertions(+), 1 deletion(-)
+
+--- a/pyproject.toml
++++ b/pyproject.toml
+@@ -86,7 +86,13 @@ ignore_missing_imports = true
+ [tool.pytest.ini_options]
+ addopts = ["-rxXs", "--strict-config", "--strict-markers"]
+ markers = ["copied_from(source, changes=None): mark test as copied from
somewhere else, along with a description of changes made to accodomate e.g. our
test setup"]
+-filterwarnings = ["error"]
++filterwarnings = [
++ "error",
++ "ignore:trio.MultiError is deprecated",
++ "ignore:unclosed <(socket.socket|ssl.SSLSocket) .*:ResourceWarning",
++ "ignore:ssl.wrap_socket() is deprecated, use
SSLContext.wrap_socket():DeprecationWarning",
++ "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning"
++]
- [coverage:run]
- omit = venv/*, httpcore/_sync/*
+ [tool.coverage.run]
+ omit = [