Skip to content

Commit b312e20

Browse files
committed
feat: Use standard library zstd (3.14+) if available
1 parent 4fb9528 commit b312e20

3 files changed

Lines changed: 24 additions & 12 deletions

File tree

httpx/_decoders.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@
2626
brotli = None
2727

2828

29-
# Zstandard support is optional
29+
# Zstandard support is avaible in the standard library from 3.14 and later,
30+
# or by an optional dependency on `zstandard`
3031
try:
31-
import zstandard
32+
import compression.zstd as zstandard
3233
except ImportError: # pragma: no cover
33-
zstandard = None # type: ignore
34+
try:
35+
import zstandard
36+
except ImportError: # pragma: no cover
37+
zstandard = None
3438

3539

3640
class ContentDecoder:
@@ -174,9 +178,16 @@ def __init__(self) -> None:
174178
"Make sure to install httpx using `pip install httpx[zstd]`."
175179
) from None
176180

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

184+
def _new_decompressor(self) -> None:
185+
decompressor = zstandard.ZstdDecompressor()
186+
if hasattr(decompressor, "decompressobj"):
187+
self.decompressor = decompressor.decompressobj() # prgama: no cover
188+
else:
189+
self.decompressor = decompressor # pragma: no cover
190+
180191
def decode(self, data: bytes) -> bytes:
181192
assert zstandard is not None
182193
self.seen_data = True
@@ -185,19 +196,16 @@ def decode(self, data: bytes) -> bytes:
185196
output.write(self.decompressor.decompress(data))
186197
while self.decompressor.eof and self.decompressor.unused_data:
187198
unused_data = self.decompressor.unused_data
188-
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
199+
self._new_decompressor()
189200
output.write(self.decompressor.decompress(unused_data))
190201
except zstandard.ZstdError as exc:
191202
raise DecodingError(str(exc)) from exc
192203
return output.getvalue()
193204

194205
def flush(self) -> bytes:
195-
if not self.seen_data:
196-
return b""
197-
ret = self.decompressor.flush() # note: this is a no-op
198-
if not self.decompressor.eof:
206+
if self.seen_data and not self.decompressor.eof:
199207
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
200-
return bytes(ret)
208+
return b""
201209

202210

203211
class MultiDecoder(ContentDecoder):

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+
"zstandard>=0.18.0; python_version < \"3.14\"",
5656
]
5757

5858
[project.scripts]

tests/test_decoders.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
import chardet
88
import pytest
9-
import zstandard as zstd
9+
10+
try:
11+
from compression import zstd
12+
except ImportError:
13+
import zstandard as zstd
1014

1115
import httpx
1216

0 commit comments

Comments
 (0)