|
25 | 25 | except ImportError: |
26 | 26 | brotli = None |
27 | 27 |
|
28 | | - |
29 | | -# Zstandard support is optional |
| 28 | +# Zstandard support is optional on Python < 3.14 |
30 | 29 | try: |
31 | | - import zstandard |
32 | | -except ImportError: # pragma: no cover |
33 | | - zstandard = None # type: ignore |
| 30 | + from compression import zstd |
| 31 | +except ImportError: |
| 32 | + try: |
| 33 | + from backports import zstd |
| 34 | + except ImportError: |
| 35 | + zstd = None |
34 | 36 |
|
35 | 37 |
|
36 | 38 | class ContentDecoder: |
@@ -162,32 +164,34 @@ class ZStandardDecoder(ContentDecoder): |
162 | 164 | """ |
163 | 165 | Handle 'zstd' RFC 8878 decoding. |
164 | 166 |
|
165 | | - Requires `pip install zstandard`. |
| 167 | + Requires `pip install backports.zstd` on Python < 3.14. |
166 | 168 | Can be installed as a dependency of httpx using `pip install httpx[zstd]`. |
167 | 169 | """ |
168 | 170 |
|
169 | 171 | # inspired by the ZstdDecoder implementation in urllib3 |
170 | 172 | def __init__(self) -> None: |
171 | | - if zstandard is None: # pragma: no cover |
| 173 | + if zstd is None: # pragma: no cover |
172 | 174 | raise ImportError( |
173 | 175 | "Using 'ZStandardDecoder', ..." |
174 | 176 | "Make sure to install httpx using `pip install httpx[zstd]`." |
175 | 177 | ) from None |
176 | 178 |
|
177 | | - self.decompressor = zstandard.ZstdDecompressor().decompressobj() |
| 179 | + self.decompressor = zstd |
| 180 | + self.decompressor.eof = None |
| 181 | + self.decompressor.unused_data = None |
178 | 182 | self.seen_data = False |
179 | 183 |
|
180 | 184 | def decode(self, data: bytes) -> bytes: |
181 | | - assert zstandard is not None |
| 185 | + assert zstd is not None |
182 | 186 | self.seen_data = True |
183 | 187 | output = io.BytesIO() |
184 | 188 | try: |
185 | 189 | output.write(self.decompressor.decompress(data)) |
186 | 190 | while self.decompressor.eof and self.decompressor.unused_data: |
187 | 191 | unused_data = self.decompressor.unused_data |
188 | | - self.decompressor = zstandard.ZstdDecompressor().decompressobj() |
| 192 | + self.decompressor = zstd |
189 | 193 | output.write(self.decompressor.decompress(unused_data)) |
190 | | - except zstandard.ZstdError as exc: |
| 194 | + except zstd.ZstdError as exc: |
191 | 195 | raise DecodingError(str(exc)) from exc |
192 | 196 | return output.getvalue() |
193 | 197 |
|
@@ -389,5 +393,5 @@ def flush(self) -> list[str]: |
389 | 393 |
|
390 | 394 | if brotli is None: |
391 | 395 | SUPPORTED_DECODERS.pop("br") # pragma: no cover |
392 | | -if zstandard is None: |
| 396 | +if zstd is None: |
393 | 397 | SUPPORTED_DECODERS.pop("zstd") # pragma: no cover |
0 commit comments