Skip to content

Commit

Permalink
Release 2.12.909 (#204)
Browse files Browse the repository at this point in the history
2.12.909 (2024-01-20)
=====================

- Fixed compatibility with upstream urllib3 when third party program
invoke deprecated ``HTTPResponse.getheader`` or
``HTTPResponse.getheaders``. Those methods were planned to be removed in
2.1 (they still have a pending deprecation
that mention 2.1 target in the 2.3 version). As such we immediately
restore the methods. (#203)
- Implemented our copy of ``HTTPResponse.read1`` heavily simplified as
we do already support ``HTTPResponse.read(-1)``.
  Also mirrored in ``AsyncHTTPResponse.read1``.
- Automatically grab ``qh3`` for X86 based processors (e.g. win32).
- Fixed the aggressive exception in our native websocket implementation
when a server responded positively to an upgrade
request without the required http header. Instead of ``RuntimeError``,
now raises ``ProtocolError``.
  • Loading branch information
Ousret authored Jan 21, 2025
2 parents 67f8a3d + 992213d commit cf62791
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 20 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ jobs:

- name: "Traefik: Prerequisites - Colima (MacOS)"
if: ${{ matrix.traefik-server && contains(matrix.os, 'mac') }}
uses: douglascamata/setup-docker-macos-action@4fe96839fcba8a2d746e020d00a89a37afbc7dc9
uses: douglascamata/setup-docker-macos-action@f2b307ddc57e8b9d3f9761f3cafa8883b2cdffd4
with:
colima-network-address: true

Expand All @@ -167,9 +167,9 @@ jobs:
TRAEFIK_HTTPBIN_IPV4: ${{ contains(matrix.os, 'mac') && '192.168.65.2' || '127.0.0.1' }}

- name: "Upload artifact"
uses: "actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce"
uses: "actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808"
with:
name: coverage-data
name: coverage-data-${{ matrix.os }}-${{ matrix.nox-session }}-${{ matrix.python-version }}-${{ matrix.traefik-server }}
path: ".coverage.*"
if-no-files-found: error

Expand All @@ -190,9 +190,10 @@ jobs:
run: "python -m pip install --upgrade coverage"

- name: "Download artifact"
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: coverage-data
pattern: coverage-data*
merge-multiple: true

- name: "Combine & check coverage"
run: |
Expand All @@ -201,7 +202,7 @@ jobs:
python -m coverage report --ignore-errors --show-missing --fail-under=86
- name: "Upload report"
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
with:
name: coverage-report
path: htmlcov
10 changes: 6 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
python-version: "3.x"

- name: "Install dependencies"
run: python -m pip install build==0.8.0
run: python -m pip install build

- name: "Build dists"
run: |
Expand All @@ -38,7 +38,7 @@ jobs:
cd dist && echo "::set-output name=hashes::$(sha256sum * | base64 -w0)"
- name: "Upload dists"
uses: "actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce"
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808
with:
name: "dist"
path: "dist/"
Expand All @@ -51,7 +51,7 @@ jobs:
actions: read
contents: write
id-token: write # Needed to access the workflow's OIDC identity.
uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0"
uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0"
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true
Expand All @@ -71,7 +71,7 @@ jobs:

steps:
- name: "Download dists"
uses: "actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a"
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e
with:
name: "dist"
path: "dist/"
Expand All @@ -84,3 +84,5 @@ jobs:
- name: "Publish dists to PyPI"
uses: "pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70" # v1.12.3
with:
attestations: true
12 changes: 12 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
2.12.909 (2024-01-20)
=====================

- Fixed compatibility with upstream urllib3 when third party program invoke deprecated ``HTTPResponse.getheader`` or
``HTTPResponse.getheaders``. Those methods were planned to be removed in 2.1 (they still have a pending deprecation
that mention 2.1 target in the 2.3 version). As such we immediately restore the methods. (#203)
- Implemented our copy of ``HTTPResponse.read1`` heavily simplified as we do already support ``HTTPResponse.read(-1)``.
Also mirrored in ``AsyncHTTPResponse.read1``.
- Automatically grab ``qh3`` for X86/i686 based processors (e.g. win32).
- Fixed the aggressive exception in our native websocket implementation when a server responded positively to an upgrade
request without the required http header. Instead of ``RuntimeError``, now raises ``ProtocolError``.

2.12.908 (2024-01-13)
=====================

Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ automatically available depending on the availability of the wheel on your platf
This may require some external toolchain to be available (compilation).

.. note:: HTTP/3 is automatically installed and ready-to-use if you fulfill theses requirements: Linux, Windows or MacOS using Python (or PyPy) 3.7 onward with one of the supported architecture (arm64/aarch64/s390x/x86_64/amd64/ppc64/ppc64le).
.. note:: HTTP/3 is automatically installed and ready-to-use if you fulfill theses requirements: Linux, Windows or MacOS using Python (or PyPy) 3.7 onward with one of the supported architecture (aarch64/armv7l/s390x/x86_64/x86/i686/amd64/ppc64(le)).

.. caution:: If the requirements aren't fulfilled for HTTP/3, your package manager won't pick qh3 for installation when installing urllib3-future and it will be silently disabled. We choose not to impose compilation and keep a safe pure Python fallback.

Expand Down
4 changes: 3 additions & 1 deletion dummyserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,11 @@ async def inner_fn() -> R:
try:
return asyncio.run(inner_fn())
finally:
# AttributeError -> Python 3.14!
# todo: investigate...
try:
tornado_loop.close(all_fds=True) # type: ignore[union-attr]
except (ValueError, OSError):
except (ValueError, OSError, AttributeError):
pass # can fail needlessly with "Invalid file descriptor". Ignore!


Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ classifiers = [
requires-python = ">=3.7"
dynamic = ["version"]
dependencies = [
"qh3>=1.2.0,<2.0.0; (platform_python_implementation != 'CPython' or python_full_version > '3.7.10') and (platform_system == 'Darwin' or platform_system == 'Windows' or platform_system == 'Linux') and (platform_machine == 'x86_64' or platform_machine == 's390x' or platform_machine == 'aarch64' or platform_machine == 'armv7l' or platform_machine == 'ppc64le' or platform_machine == 'ppc64' or platform_machine == 'AMD64' or platform_machine == 'arm64' or platform_machine == 'ARM64') and (platform_python_implementation == 'CPython' or (platform_python_implementation == 'PyPy' and python_version < '3.11'))",
"qh3>=1.2.0,<2.0.0; (platform_python_implementation != 'CPython' or python_full_version > '3.7.10') and (platform_system == 'Darwin' or platform_system == 'Windows' or platform_system == 'Linux') and (platform_machine == 'x86_64' or platform_machine == 's390x' or platform_machine == 'aarch64' or platform_machine == 'armv7l' or platform_machine == 'ppc64le' or platform_machine == 'ppc64' or platform_machine == 'AMD64' or platform_machine == 'arm64' or platform_machine == 'ARM64' or platform_machine == 'x86' or platform_machine == 'i686') and (platform_python_implementation == 'CPython' or (platform_python_implementation == 'PyPy' and python_version < '3.11'))",
"h11>=0.11.0,<1.0.0",
"jh2>=5.0.3,<6.0.0",
]
Expand Down Expand Up @@ -108,6 +108,9 @@ log_level = "DEBUG"
filterwarnings = [
"error",
'''ignore:.*iscoroutinefunction.*:DeprecationWarning''',
'''ignore:.*get_event_loop.*:DeprecationWarning''',
'''ignore:.*set_event_loop.*:DeprecationWarning''',
'''ignore:.*EventLoopPolicy.*:DeprecationWarning''',
'''default:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning''',
'''default:ssl\.PROTOCOL_TLSv1 is deprecated:DeprecationWarning''',
'''default:ssl\.TLSVersion\.TLSv1_1 is deprecated:DeprecationWarning''',
Expand Down
29 changes: 29 additions & 0 deletions src/urllib3/_async/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,35 @@ async def _raw_read( # type: ignore[override]

return data

async def read1( # type: ignore[override]
self,
amt: int | None = None,
decode_content: bool | None = None,
) -> bytes:
"""
Similar to ``http.client.HTTPResponse.read1`` and documented
in :meth:`io.BufferedReader.read1`, but with an additional parameter:
``decode_content``.
:param amt:
How much of the content to read.
:param decode_content:
If True, will attempt to decode the body based on the
'content-encoding' header.
"""

data = await self.read(
amt=amt or -1,
decode_content=decode_content,
)

if amt is not None and len(data) > amt:
self._decoded_buffer.put(data)
return self._decoded_buffer.get(amt)

return data

async def read( # type: ignore[override]
self,
amt: int | None = None,
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.12.908"
__version__ = "2.12.909"
4 changes: 1 addition & 3 deletions src/urllib3/contrib/webextensions/_async/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async def start(self, response: AsyncHTTPResponse) -> None:
accept_token: str | None = response.headers.get("Sec-Websocket-Accept")

if accept_token is None:
raise RuntimeError(
raise ProtocolError(
"The WebSocket HTTP extension requires 'Sec-Websocket-Accept' header in the server response but was not present."
)

Expand Down Expand Up @@ -128,8 +128,6 @@ async def close(self) -> None:
if self._response is not None:
if self._police_officer is not None:
self._police_officer.forget(self._response)
if self._police_officer.busy:
self._police_officer.release()
else:
await self._response.close()
self._response = None
Expand Down
4 changes: 1 addition & 3 deletions src/urllib3/contrib/webextensions/ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def start(self, response: HTTPResponse) -> None:
accept_token: str | None = response.headers.get("Sec-Websocket-Accept")

if accept_token is None:
raise RuntimeError(
raise ProtocolError(
"The WebSocket HTTP extension requires 'Sec-Websocket-Accept' header in the server response but was not present."
)

Expand Down Expand Up @@ -128,8 +128,6 @@ def close(self) -> None:
if self._response is not None:
if self._police_officer is not None:
self._police_officer.forget(self._response)
if self._police_officer.busy:
self._police_officer.release()
else:
self._response.close()
self._response = None
Expand Down
49 changes: 49 additions & 0 deletions src/urllib3/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
import sys
import typing
import warnings
import zlib
from contextlib import contextmanager
from socket import timeout as SocketTimeout
Expand Down Expand Up @@ -834,6 +835,35 @@ def _raw_read(

return data

def read1(
self,
amt: int | None = None,
decode_content: bool | None = None,
) -> bytes:
"""
Similar to ``http.client.HTTPResponse.read1`` and documented
in :meth:`io.BufferedReader.read1`, but with an additional parameter:
``decode_content``.
:param amt:
How much of the content to read.
:param decode_content:
If True, will attempt to decode the body based on the
'content-encoding' header.
"""

data = self.read(
amt=amt or -1,
decode_content=decode_content,
)

if amt is not None and len(data) > amt:
self._decoded_buffer.put(data)
return self._decoded_buffer.get(amt)

return data

def read(
self,
amt: int | None = None,
Expand Down Expand Up @@ -1037,6 +1067,25 @@ def url(self) -> str | None:
def url(self, url: str) -> None:
self._request_url = url

# Compatibility methods for http.client.HTTPResponse
def getheaders(self) -> HTTPHeaderDict:
warnings.warn(
"HTTPResponse.getheaders() is deprecated and will be removed "
"in a future version of urllib3(-future). Instead access HTTPResponse.headers directly.",
category=DeprecationWarning,
stacklevel=2,
)
return self.headers

def getheader(self, name: str, default: str | None = None) -> str | None:
warnings.warn(
"HTTPResponse.getheader() is deprecated and will be removed "
"in a future version of urllib3(-future). Instead use HTTPResponse.headers.get(name, default).",
category=DeprecationWarning,
stacklevel=2,
)
return self.headers.get(name, default)

def __iter__(self) -> typing.Iterator[bytes]:
buffer: list[bytes] = []
for chunk in self.stream(-1, decode_content=True):
Expand Down

0 comments on commit cf62791

Please sign in to comment.