|
8 | 8 |
|
9 | 9 | import codecs |
10 | 10 | import io |
| 11 | +import sys |
11 | 12 | import typing |
12 | 13 | import zlib |
13 | 14 |
|
|
28 | 29 |
|
29 | 30 | # Zstandard support is optional |
30 | 31 | 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 |
32 | 36 | except ImportError: # pragma: no cover |
33 | | - zstandard = None # type: ignore |
| 37 | + zstd = None # type: ignore |
34 | 38 |
|
35 | 39 |
|
36 | 40 | class ContentDecoder: |
@@ -162,42 +166,41 @@ class ZStandardDecoder(ContentDecoder): |
162 | 166 | """ |
163 | 167 | Handle 'zstd' RFC 8878 decoding. |
164 | 168 |
|
165 | | - Requires `pip install zstandard`. |
| 169 | + Requires `pip install backports.zstd` for Python before 3.14. |
166 | 170 | Can be installed as a dependency of httpx using `pip install httpx[zstd]`. |
167 | 171 | """ |
168 | 172 |
|
169 | 173 | # inspired by the ZstdDecoder implementation in urllib3 |
170 | 174 | def __init__(self) -> None: |
171 | | - if zstandard is None: # pragma: no cover |
| 175 | + if zstd is None: # pragma: no cover |
172 | 176 | raise ImportError( |
173 | 177 | "Using 'ZStandardDecoder', ..." |
174 | 178 | "Make sure to install httpx using `pip install httpx[zstd]`." |
175 | 179 | ) from None |
176 | 180 |
|
177 | | - self.decompressor = zstandard.ZstdDecompressor().decompressobj() |
| 181 | + self.decompressor = zstd.ZstdDecompressor() |
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.ZstdDecompressor() |
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 |
|
194 | 198 | def flush(self) -> bytes: |
195 | 199 | if not self.seen_data: |
196 | 200 | return b"" |
197 | | - ret = self.decompressor.flush() # note: this is a no-op |
198 | 201 | if not self.decompressor.eof: |
199 | 202 | raise DecodingError("Zstandard data is incomplete") # pragma: no cover |
200 | | - return bytes(ret) |
| 203 | + return b"" |
201 | 204 |
|
202 | 205 |
|
203 | 206 | class MultiDecoder(ContentDecoder): |
@@ -389,5 +392,5 @@ def flush(self) -> list[str]: |
389 | 392 |
|
390 | 393 | if brotli is None: |
391 | 394 | SUPPORTED_DECODERS.pop("br") # pragma: no cover |
392 | | -if zstandard is None: |
| 395 | +if zstd is None: |
393 | 396 | SUPPORTED_DECODERS.pop("zstd") # pragma: no cover |
0 commit comments