Skip to content

Commit 4d05db3

Browse files
committed
Use zstandard implementation from stdlib (PEP-784)
1 parent ae1b9f6 commit 4d05db3

File tree

5 files changed

+24
-16
lines changed

5 files changed

+24
-16
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ As well as these optional installs:
136136
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
137137
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
138138
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
139-
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
139+
* `backports.zstd` - Decoding for "zstd" compressed responses on Python before 3.14. *(Optional, with `httpx[zstd]`)*
140140

141141
A huge amount of credit is due to `requests` for the API layout that
142142
much of this work follows, as well as to `urllib3` for plenty of design

docs/quickstart.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
100100

101101
Any `gzip` and `deflate` HTTP response encodings will automatically
102102
be decoded for you. If `brotlipy` is installed, then the `brotli` response
103-
encoding will be supported. If `zstandard` is installed, then `zstd`
104-
response encodings will also be supported.
103+
encoding will be supported. If the Python version used is 3.14 or higher or
104+
if `backports.zstd` is installed, then `zstd` response encodings will also be supported.
105105

106106
For example, to create an image from binary data returned by a request, you can use the following code:
107107

httpx/_decoders.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import codecs
1010
import io
11+
import sys
1112
import typing
1213
import zlib
1314

@@ -28,9 +29,12 @@
2829

2930
# Zstandard support is optional
3031
try:
31-
import zstandard
32+
if sys.version_info >= (3, 14):
33+
from compression import zstd # pragma: no cover
34+
else:
35+
from backports import zstd # pragma: no cover
3236
except ImportError: # pragma: no cover
33-
zstandard = None # type: ignore
37+
zstd = None # type: ignore
3438

3539

3640
class ContentDecoder:
@@ -162,42 +166,41 @@ class ZStandardDecoder(ContentDecoder):
162166
"""
163167
Handle 'zstd' RFC 8878 decoding.
164168
165-
Requires `pip install zstandard`.
169+
Requires `pip install backports.zstd` for Python before 3.14.
166170
Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
167171
"""
168172

169173
# inspired by the ZstdDecoder implementation in urllib3
170174
def __init__(self) -> None:
171-
if zstandard is None: # pragma: no cover
175+
if zstd is None: # pragma: no cover
172176
raise ImportError(
173177
"Using 'ZStandardDecoder', ..."
174178
"Make sure to install httpx using `pip install httpx[zstd]`."
175179
) from None
176180

177-
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
181+
self.decompressor = zstd.ZstdDecompressor()
178182
self.seen_data = False
179183

180184
def decode(self, data: bytes) -> bytes:
181-
assert zstandard is not None
185+
assert zstd is not None
182186
self.seen_data = True
183187
output = io.BytesIO()
184188
try:
185189
output.write(self.decompressor.decompress(data))
186190
while self.decompressor.eof and self.decompressor.unused_data:
187191
unused_data = self.decompressor.unused_data
188-
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
192+
self.decompressor = zstd.ZstdDecompressor()
189193
output.write(self.decompressor.decompress(unused_data))
190-
except zstandard.ZstdError as exc:
194+
except zstd.ZstdError as exc:
191195
raise DecodingError(str(exc)) from exc
192196
return output.getvalue()
193197

194198
def flush(self) -> bytes:
195199
if not self.seen_data:
196200
return b""
197-
ret = self.decompressor.flush() # note: this is a no-op
198201
if not self.decompressor.eof:
199202
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
200-
return bytes(ret)
203+
return b""
201204

202205

203206
class MultiDecoder(ContentDecoder):
@@ -389,5 +392,5 @@ def flush(self) -> list[str]:
389392

390393
if brotli is None:
391394
SUPPORTED_DECODERS.pop("br") # pragma: no cover
392-
if zstandard is None:
395+
if zstd is None:
393396
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ socks = [
5252
"socksio==1.*",
5353
]
5454
zstd = [
55-
"zstandard>=0.18.0",
55+
"backports.zstd>=1.0.0 ; python_version < '3.14'",
5656
]
5757

5858
[project.scripts]

tests/test_decoders.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
from __future__ import annotations
22

33
import io
4+
import sys
45
import typing
56
import zlib
67

78
import chardet
89
import pytest
9-
import zstandard as zstd
1010

1111
import httpx
1212

13+
if sys.version_info >= (3, 14):
14+
from compression import zstd # pragma: no cover
15+
else:
16+
from backports import zstd # pragma: no cover
17+
1318

1419
def test_deflate():
1520
"""

0 commit comments

Comments
 (0)