Skip to content

Commit

Permalink
🔖 Release 2.8.902 (#129)
Browse files Browse the repository at this point in the history
- Added support for async iterable yielding either bytes or str when
passing a body into your requests.
- Added dummy module (e.g. http2 and emscriptem) like upstream without
serving any of them. Those modules won't be served and are empty as we
diverged since.
- Added a better error message for http3 handshake failure to help out
users figuring out what is happening.
- Added official support for Python 3.13
  • Loading branch information
Ousret authored Jul 7, 2024
1 parent b8037d0 commit 0ab877e
Show file tree
Hide file tree
Showing 18 changed files with 209 additions and 43 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
os:
- macos-12
- windows-latest
Expand Down Expand Up @@ -242,6 +242,7 @@ jobs:
uses: "actions/setup-python@61a6322f88396a6271a6ee3565807d608ecaddd1"
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true

- name: "Install dependencies"
run: python -m pip install --upgrade pip setuptools nox
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
2.8.902 (2024-07-07)
====================

- Added support for async iterable yielding either bytes or str when passing a body into your requests.
- Added dummy module (e.g. http2 and emscriptem) like upstream without serving any of them. Those modules won't be served and are empty as we diverged since.
- Added a better error message for http3 handshake failure to help out users figuring out what is happening.
- Added official support for Python 3.13

2.8.901 (2024-06-27)
====================

Expand Down
4 changes: 3 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
coverage>=7.2.7,<=7.4
coverage>=7.2.7,<=7.4.1
tornado>=6.2,<=6.4
python-socks==2.4.4
pytest==7.4.4
Expand All @@ -11,3 +11,5 @@ cryptography==42.0.5;implementation_name!="pypy" or implementation_version>="7.3
backports.zoneinfo==0.2.1;python_version<"3.9"
towncrier==21.9.0
pytest-asyncio>=0.21.1,<=0.23.5.post1
# unblock cffi for Python 3.13
cffi==1.17.0rc1; python_version > "3.12"
5 changes: 5 additions & 0 deletions docs/async.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,8 @@ The following properties and methods are awaitable:

In addition to that, ``AsyncHTTPResponse`` ships with an async iterator.

Sending async iterable
----------------------

In our asynchronous APIs, you can send async iterable using the ``body=...`` keyword argument.
It is most useful when trying to send files that are IO bound, thus blocking your event loop needlessly.
4 changes: 2 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

def tests_impl(
session: nox.Session,
extras: str = "socks,brotli,zstd",
extras: str = "socks,brotli",
byte_string_comparisons: bool = False,
) -> None:
# Install deps and the package itself.
Expand Down Expand Up @@ -44,7 +44,7 @@ def tests_impl(
)


@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy"])
@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy"])
def test(session: nox.Session) -> None:
tests_impl(session)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ authors = [
{name = "Andrey Petrov", email = "[email protected]"}
]
maintainers = [
{name = "Ahmed R. TAHRI", email="ahmed.tahri@cloudnursery.dev"},
{name = "Ahmed R. TAHRI", email="tahri[email protected]"},
]
classifiers = [
"Environment :: Web Environment",
Expand Down
40 changes: 28 additions & 12 deletions src/urllib3/_async/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
_TYPE_SOCKET_OPTIONS,
_TYPE_TIMEOUT_INTERNAL,
ProxyConfig,
_TYPE_ASYNC_BODY,
)
from ..util._async.traffic_police import AsyncTrafficPolice

Expand Down Expand Up @@ -324,7 +325,7 @@ async def request(
self,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
*,
chunked: bool = False,
Expand Down Expand Up @@ -459,17 +460,32 @@ async def request(
try:
# If we're given a body we start sending that in chunks.
if chunks is not None:
for chunk in chunks:
# Sending empty chunks isn't allowed for TE: chunked
# as it indicates the end of the body.
if not chunk:
continue
if isinstance(chunk, str):
chunk = chunk.encode("utf-8")
await self.send(chunk)
total_sent += len(chunk)
if on_upload_body is not None:
await on_upload_body(total_sent, content_length, False, False)
if hasattr(chunks, "__aiter__"):
async for chunk in chunks:
if not chunk:
continue
if isinstance(chunk, str):
chunk = chunk.encode("utf-8")
await self.send(chunk)
total_sent += len(chunk)
if on_upload_body is not None:
await on_upload_body(
total_sent, content_length, False, False
)
else:
for chunk in chunks:
# Sending empty chunks isn't allowed for TE: chunked
# as it indicates the end of the body.
if not chunk:
continue
if isinstance(chunk, str):
chunk = chunk.encode("utf-8")
await self.send(chunk)
total_sent += len(chunk)
if on_upload_body is not None:
await on_upload_body(
total_sent, content_length, False, False
)
try:
rp = await self.send(b"", eot=True)
except TypeError:
Expand Down
20 changes: 13 additions & 7 deletions src/urllib3/_async/connectionpool.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@

from .._collections import HTTPHeaderDict
from .._request_methods import AsyncRequestMethods
from .._typing import _TYPE_BODY, _TYPE_BODY_POSITION, _TYPE_TIMEOUT, ProxyConfig
from .._typing import (
_TYPE_ASYNC_BODY,
_TYPE_BODY,
_TYPE_BODY_POSITION,
_TYPE_TIMEOUT,
ProxyConfig,
)
from ..backend import ConnectionInfo, ResponsePromise
from ..connection import _wrap_proxy_error
from ..connectionpool import _normalize_host
Expand Down Expand Up @@ -851,7 +857,7 @@ async def _make_request(
conn: AsyncHTTPConnection,
method: str,
url: str,
body: _TYPE_BODY | None = ...,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = ...,
headers: typing.Mapping[str, str] | None = ...,
retries: Retry | None = ...,
timeout: _TYPE_TIMEOUT = ...,
Expand All @@ -876,7 +882,7 @@ async def _make_request(
conn: AsyncHTTPConnection,
method: str,
url: str,
body: _TYPE_BODY | None = ...,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = ...,
headers: typing.Mapping[str, str] | None = ...,
retries: Retry | None = ...,
timeout: _TYPE_TIMEOUT = ...,
Expand All @@ -900,7 +906,7 @@ async def _make_request(
conn: AsyncHTTPConnection,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
retries: Retry | None = None,
timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
Expand Down Expand Up @@ -1151,7 +1157,7 @@ async def urlopen(
self,
method: str,
url: str,
body: _TYPE_BODY | None = ...,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = ...,
headers: typing.Mapping[str, str] | None = ...,
retries: Retry | bool | int | None = ...,
redirect: bool = ...,
Expand Down Expand Up @@ -1179,7 +1185,7 @@ async def urlopen(
self,
method: str,
url: str,
body: _TYPE_BODY | None = ...,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = ...,
headers: typing.Mapping[str, str] | None = ...,
retries: Retry | bool | int | None = ...,
redirect: bool = ...,
Expand All @@ -1206,7 +1212,7 @@ async def urlopen(
self,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
retries: Retry | bool | int | None = None,
redirect: bool = True,
Expand Down
14 changes: 7 additions & 7 deletions src/urllib3/_request_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from ._async.response import AsyncHTTPResponse
from ._collections import HTTPHeaderDict
from ._typing import _TYPE_BODY, _TYPE_ENCODE_URL_FIELDS, _TYPE_FIELDS
from ._typing import _TYPE_ASYNC_BODY, _TYPE_BODY, _TYPE_ENCODE_URL_FIELDS, _TYPE_FIELDS
from .filepost import encode_multipart_formdata
from .response import HTTPResponse

Expand Down Expand Up @@ -377,7 +377,7 @@ async def urlopen(
self,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
encode_multipart: bool = True,
multipart_boundary: str | None = None,
Expand All @@ -392,7 +392,7 @@ async def urlopen(
self,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
encode_multipart: bool = True,
multipart_boundary: str | None = None,
Expand All @@ -406,7 +406,7 @@ async def urlopen(
self,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
headers: typing.Mapping[str, str] | None = None,
encode_multipart: bool = True,
multipart_boundary: str | None = None,
Expand All @@ -422,7 +422,7 @@ async def request(
self,
method: str,
url: str,
body: _TYPE_BODY | None = ...,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = ...,
fields: _TYPE_FIELDS | None = ...,
headers: typing.Mapping[str, str] | None = ...,
json: typing.Any | None = ...,
Expand All @@ -437,7 +437,7 @@ async def request(
self,
method: str,
url: str,
body: _TYPE_BODY | None = ...,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = ...,
fields: _TYPE_FIELDS | None = ...,
headers: typing.Mapping[str, str] | None = ...,
json: typing.Any | None = ...,
Expand All @@ -451,7 +451,7 @@ async def request(
self,
method: str,
url: str,
body: _TYPE_BODY | None = None,
body: _TYPE_BODY | _TYPE_ASYNC_BODY | None = None,
fields: _TYPE_FIELDS | None = None,
headers: typing.Mapping[str, str] | None = None,
json: typing.Any | None = None,
Expand Down
5 changes: 5 additions & 0 deletions src/urllib3/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class _TYPE_PEER_CERT_RET_DICT(TypedDict, total=False):
AsyncLowLevelResponse,
]

_TYPE_ASYNC_BODY: typing.TypeAlias = typing.Union[
typing.AsyncIterable[bytes],
typing.AsyncIterable[str],
]

_TYPE_FIELD_VALUE: typing.TypeAlias = typing.Union[str, bytes]
_TYPE_FIELD_VALUE_TUPLE: typing.TypeAlias = typing.Union[
_TYPE_FIELD_VALUE,
Expand Down
2 changes: 1 addition & 1 deletion src/urllib3/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This file is protected via CODEOWNERS
from __future__ import annotations

__version__ = "2.8.901"
__version__ = "2.8.902"
16 changes: 12 additions & 4 deletions src/urllib3/backend/_async/hface.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,18 @@ async def _post_conn(self) -> None: # type: ignore[override]
return

# it may be required to send some initial data, aka. magic header (PRI * HTTP/2..)
await self.__exchange_until(
HandshakeCompleted,
receive_first=False,
)
try:
await self.__exchange_until(
HandshakeCompleted,
receive_first=False,
)
except ProtocolError as e:
if isinstance(self._protocol, HTTPOverQUICProtocol):
raise ProtocolError(
"It is likely that the server yielded its support for HTTP/3 through the Alt-Svc header while unable to do so. "
"To remediate that issue, either disable http3 or reach out to the server admin."
) from e
raise

if isinstance(self._protocol, HTTPOverQUICProtocol):
self.conn_info.certificate_der = self._protocol.getpeercert(
Expand Down
16 changes: 12 additions & 4 deletions src/urllib3/backend/hface.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,18 @@ def _post_conn(self) -> None:
return

# it may be required to send some initial data, aka. magic header (PRI * HTTP/2..)
self.__exchange_until(
HandshakeCompleted,
receive_first=False,
)
try:
self.__exchange_until(
HandshakeCompleted,
receive_first=False,
)
except ProtocolError as e:
if isinstance(self._protocol, HTTPOverQUICProtocol):
raise ProtocolError(
"It is likely that the server yielded its support for HTTP/3 through the Alt-Svc header while unable to do so. "
"To remediate that issue, either disable http3 or reach out to the server admin."
) from e
raise

#: Populating ConnectionInfo using QUIC TLS interfaces
if isinstance(self._protocol, HTTPOverQUICProtocol):
Expand Down
4 changes: 4 additions & 0 deletions src/urllib3/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ def request(
try:
# If we're given a body we start sending that in chunks.
if chunks is not None:
if hasattr(chunks, "__aiter__"):
raise RuntimeError(
"Unable to send an async iterable through a synchronous connection"
)
for chunk in chunks:
# Sending empty chunks isn't allowed for TE: chunked
# as it indicates the end of the body.
Expand Down
24 changes: 24 additions & 0 deletions src/urllib3/contrib/emscripten/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Dummy file to match upstream modules
# without actually serving them.
# urllib3-future diverged from urllib3.
# only the top-level (public API) are guaranteed to be compatible.
# in-fact urllib3-future propose a better way to migrate/transition toward
# newer protocols.

from __future__ import annotations

import warnings


def inject_into_urllib3() -> None:
warnings.warn(
"urllib3-future do not have a emscripten module as it is irrelevant to urllib3 nature. "
"wasm support will be brought in Niquests (replacement for Requests). "
"One does not simply ship an addon that essentially kills 90% of its other features and alter the 10 "
"remaining percents.",
UserWarning,
)


def extract_from_urllib3() -> None:
pass
22 changes: 22 additions & 0 deletions src/urllib3/http2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Dummy file to match upstream modules
# without actually serving them.
# urllib3-future diverged from urllib3.
# only the top-level (public API) are guaranteed to be compatible.
# in-fact urllib3-future propose a better way to migrate/transition toward
# newer protocols.

from __future__ import annotations

import warnings


def inject_into_urllib3() -> None:
warnings.warn(
"urllib3-future do not propose the http2 module as it is useless to us. "
"enjoy all three protocols. urllib3-future just works out of the box with all protocols.",
UserWarning,
)


def extract_from_urllib3() -> None:
pass
Loading

0 comments on commit 0ab877e

Please sign in to comment.