Skip to content

Commit d514312

Browse files
Use either brotli or brotlicffi. (#1618)
* Use either brotli (recommended for CPython) or brotlicffi (Recommended for PyPy and others) * Add comments in places where we switch behaviour depending on brotli/brotlicffi Co-authored-by: Florimond Manca <[email protected]>
1 parent acb5e6a commit d514312

File tree

7 files changed

+42
-25
lines changed

7 files changed

+42
-25
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ The HTTPX project relies on these excellent libraries:
124124
* `idna` - Internationalized domain name support.
125125
* `sniffio` - Async library autodetection.
126126
* `async_generator` - Backport support for `contextlib.asynccontextmanager`. *(Only required for Python 3.6)*
127-
* `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
127+
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
128128

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

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ The HTTPX project relies on these excellent libraries:
116116
* `idna` - Internationalized domain name support.
117117
* `sniffio` - Async library autodetection.
118118
* `async_generator` - Backport support for `contextlib.asynccontextmanager`. *(Only required for Python 3.6)*
119-
* `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
119+
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
120120

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

httpx/_compat.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
except ImportError:
1313
from async_generator import asynccontextmanager # type: ignore # noqa
1414

15+
# Brotli support is optional
16+
# The C bindings in `brotli` are recommended for CPython.
17+
# The CFFI bindings in `brotlicffi` are recommended for PyPy and everything else.
18+
try:
19+
import brotlicffi as brotli
20+
except ImportError: # pragma: nocover
21+
try:
22+
import brotli
23+
except ImportError:
24+
brotli = None
25+
1526
if sys.version_info >= (3, 10) or (
1627
sys.version_info >= (3, 7) and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7)
1728
):

httpx/_decoders.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,9 @@
88
import typing
99
import zlib
1010

11+
from ._compat import brotli
1112
from ._exceptions import DecodingError
1213

13-
try:
14-
import brotlicffi
15-
except ImportError: # pragma: nocover
16-
brotlicffi = None
17-
1814

1915
class ContentDecoder:
2016
def decode(self, data: bytes) -> bytes:
@@ -99,37 +95,44 @@ class BrotliDecoder(ContentDecoder):
9995
"""
10096

10197
def __init__(self) -> None:
102-
if brotlicffi is None: # pragma: nocover
98+
if brotli is None: # pragma: nocover
10399
raise ImportError(
104-
"Using 'BrotliDecoder', but the 'brotlicffi' library "
105-
"is not installed."
100+
"Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' "
101+
"packages have been installed. "
106102
"Make sure to install httpx using `pip install httpx[brotli]`."
107103
) from None
108104

109-
self.decompressor = brotlicffi.Decompressor()
105+
self.decompressor = brotli.Decompressor()
110106
self.seen_data = False
111107
if hasattr(self.decompressor, "decompress"):
112-
self._decompress = self.decompressor.decompress
108+
# The 'brotlicffi' package.
109+
self._decompress = self.decompressor.decompress # pragma: nocover
113110
else:
111+
# The 'brotli' package.
114112
self._decompress = self.decompressor.process # pragma: nocover
115113

116114
def decode(self, data: bytes) -> bytes:
117115
if not data:
118116
return b""
119117
self.seen_data = True
120118
try:
121-
return self.decompressor.decompress(data)
122-
except brotlicffi.Error as exc:
119+
return self._decompress(data)
120+
except brotli.error as exc:
123121
raise DecodingError(str(exc)) from exc
124122

125123
def flush(self) -> bytes:
126124
if not self.seen_data:
127125
return b""
128126
try:
129127
if hasattr(self.decompressor, "finish"):
130-
self.decompressor.finish()
128+
# Only available in the 'brotlicffi' package.
129+
130+
# As the decompressor decompresses eagerly, this
131+
# will never actually emit any data. However, it will potentially throw
132+
# errors if a truncated or damaged data stream has been used.
133+
self.decompressor.finish() # pragma: nocover
131134
return b""
132-
except brotlicffi.Error as exc: # pragma: nocover
135+
except brotli.error as exc: # pragma: nocover
133136
raise DecodingError(str(exc)) from exc
134137

135138

@@ -326,5 +329,5 @@ def flush(self) -> typing.List[str]:
326329
}
327330

328331

329-
if brotlicffi is None:
332+
if brotli is None:
330333
SUPPORTED_DECODERS.pop("br") # pragma: nocover

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ def get_packages(package):
6565
],
6666
extras_require={
6767
"http2": "h2>=3,<5",
68-
"brotli": "brotlicffi==1.*",
68+
"brotli": [
69+
"brotli; platform_python_implementation == 'CPython'",
70+
"brotlicffi; platform_python_implementation != 'CPython'"
71+
],
6972
},
7073
classifiers=[
7174
"Development Status :: 4 - Beta",

tests/models/test_responses.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import json
22
import pickle
33

4-
import brotlicffi
54
import pytest
65

76
import httpx
7+
from httpx._compat import brotli
88

99

1010
class StreamingBody:
@@ -798,7 +798,7 @@ def test_link_headers(headers, expected):
798798
def test_decode_error_with_request(header_value):
799799
headers = [(b"Content-Encoding", header_value)]
800800
body = b"test 123"
801-
compressed_body = brotlicffi.compress(body)[3:]
801+
compressed_body = brotli.compress(body)[3:]
802802
with pytest.raises(httpx.DecodingError):
803803
httpx.Response(
804804
200,
@@ -819,7 +819,7 @@ def test_decode_error_with_request(header_value):
819819
def test_value_error_without_request(header_value):
820820
headers = [(b"Content-Encoding", header_value)]
821821
body = b"test 123"
822-
compressed_body = brotlicffi.compress(body)[3:]
822+
compressed_body = brotli.compress(body)[3:]
823823
with pytest.raises(httpx.DecodingError):
824824
httpx.Response(200, headers=headers, content=compressed_body)
825825

tests/test_decoders.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import zlib
22

3-
import brotlicffi
43
import pytest
54

65
import httpx
6+
from httpx._compat import brotli
77
from httpx._decoders import (
88
BrotliDecoder,
99
ByteChunker,
@@ -69,7 +69,7 @@ def test_gzip():
6969

7070
def test_brotli():
7171
body = b"test 123"
72-
compressed_body = brotlicffi.compress(body)
72+
compressed_body = brotli.compress(body)
7373

7474
headers = [(b"Content-Encoding", b"br")]
7575
response = httpx.Response(
@@ -102,7 +102,7 @@ def test_multi():
102102

103103
def test_multi_with_identity():
104104
body = b"test 123"
105-
compressed_body = brotlicffi.compress(body)
105+
compressed_body = brotli.compress(body)
106106

107107
headers = [(b"Content-Encoding", b"br, identity")]
108108
response = httpx.Response(
@@ -165,7 +165,7 @@ def test_decoders_empty_cases(decoder):
165165
def test_decoding_errors(header_value):
166166
headers = [(b"Content-Encoding", header_value)]
167167
body = b"test 123"
168-
compressed_body = brotlicffi.compress(body)[3:]
168+
compressed_body = brotli.compress(body)[3:]
169169
with pytest.raises(httpx.DecodingError):
170170
request = httpx.Request("GET", "https://example.org")
171171
httpx.Response(200, headers=headers, content=compressed_body, request=request)

0 commit comments

Comments
 (0)