Use zstandard implementation from stdlib (PEP-784)

This commit is contained in:
Rogdham 2025-12-13 16:41:26 +01:00
parent ae1b9f6623
commit 4d05db33ce
No known key found for this signature in database
GPG Key ID: 3586E588AEF1642D
5 changed files with 24 additions and 16 deletions

View File

@ -136,7 +136,7 @@ As well as these optional installs:
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
* `backports.zstd` - Decoding for "zstd" compressed responses on Python before 3.14. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design

View File

@ -100,8 +100,8 @@ b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
Any `gzip` and `deflate` HTTP response encodings will automatically
be decoded for you. If `brotlipy` is installed, then the `brotli` response
encoding will be supported. If `zstandard` is installed, then `zstd`
response encodings will also be supported.
encoding will be supported. If the Python version used is 3.14 or higher or
if `backports.zstd` is installed, then `zstd` response encodings will also be supported.
For example, to create an image from binary data returned by a request, you can use the following code:

View File

@ -8,6 +8,7 @@ from __future__ import annotations
import codecs
import io
import sys
import typing
import zlib
@ -28,9 +29,12 @@ except ImportError: # pragma: no cover
# Zstandard support is optional
try:
import zstandard
if sys.version_info >= (3, 14):
from compression import zstd # pragma: no cover
else:
from backports import zstd # pragma: no cover
except ImportError: # pragma: no cover
zstandard = None # type: ignore
zstd = None # type: ignore
class ContentDecoder:
@ -162,42 +166,41 @@ class ZStandardDecoder(ContentDecoder):
"""
Handle 'zstd' RFC 8878 decoding.
Requires `pip install zstandard`.
Requires `pip install backports.zstd` for Python before 3.14.
Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
"""
# inspired by the ZstdDecoder implementation in urllib3
def __init__(self) -> None:
if zstandard is None: # pragma: no cover
if zstd is None: # pragma: no cover
raise ImportError(
"Using 'ZStandardDecoder', ..."
"Make sure to install httpx using `pip install httpx[zstd]`."
) from None
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
self.decompressor = zstd.ZstdDecompressor()
self.seen_data = False
def decode(self, data: bytes) -> bytes:
assert zstandard is not None
assert zstd is not None
self.seen_data = True
output = io.BytesIO()
try:
output.write(self.decompressor.decompress(data))
while self.decompressor.eof and self.decompressor.unused_data:
unused_data = self.decompressor.unused_data
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
self.decompressor = zstd.ZstdDecompressor()
output.write(self.decompressor.decompress(unused_data))
except zstandard.ZstdError as exc:
except zstd.ZstdError as exc:
raise DecodingError(str(exc)) from exc
return output.getvalue()
def flush(self) -> bytes:
if not self.seen_data:
return b""
ret = self.decompressor.flush() # note: this is a no-op
if not self.decompressor.eof:
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
return bytes(ret)
return b""
class MultiDecoder(ContentDecoder):
@ -389,5 +392,5 @@ SUPPORTED_DECODERS = {
if brotli is None:
SUPPORTED_DECODERS.pop("br") # pragma: no cover
if zstandard is None:
if zstd is None:
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover

View File

@ -52,7 +52,7 @@ socks = [
"socksio==1.*",
]
zstd = [
"zstandard>=0.18.0",
"backports.zstd>=1.0.0 ; python_version < '3.14'",
]
[project.scripts]

View File

@ -1,15 +1,20 @@
from __future__ import annotations
import io
import sys
import typing
import zlib
import chardet
import pytest
import zstandard as zstd
import httpx
if sys.version_info >= (3, 14):
from compression import zstd # pragma: no cover
else:
from backports import zstd # pragma: no cover
def test_deflate():
"""