]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Use either brotli or brotlicffi. (#1618)
authorTom Christie <tom@tomchristie.com>
Fri, 13 Aug 2021 10:52:45 +0000 (11:52 +0100)
committerGitHub <noreply@github.com>
Fri, 13 Aug 2021 10:52:45 +0000 (11:52 +0100)
* 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 <florimond.manca@gmail.com>
README.md
docs/index.md
httpx/_compat.py
httpx/_decoders.py
setup.py
tests/models/test_responses.py
tests/test_decoders.py

index 54c496faee69137c8cd6a001201b18c61426cc9c..268a85120fe2b8018c716c839ae3f7a87e20bba0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -124,7 +124,7 @@ The HTTPX project relies on these excellent libraries:
   * `idna` - Internationalized domain name support.
 * `sniffio` - Async library autodetection.
 * `async_generator` - Backport support for `contextlib.asynccontextmanager`. *(Only required for Python 3.6)*
-* `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
+* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
 
 A huge amount of credit is due to `requests` for the API layout that
 much of this work follows, as well as to `urllib3` for plenty of design
index 3a00ca8cbd08fe6288ced3a5c1131530875889bb..b647685970571f91f8728d2c9584049c4bbcd3ec 100644 (file)
@@ -116,7 +116,7 @@ The HTTPX project relies on these excellent libraries:
   * `idna` - Internationalized domain name support.
 * `sniffio` - Async library autodetection.
 * `async_generator` - Backport support for `contextlib.asynccontextmanager`. *(Only required for Python 3.6)*
-* `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
+* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional)*
 
 A huge amount of credit is due to `requests` for the API layout that
 much of this work follows, as well as to `urllib3` for plenty of design
index a499322f97a1e3ca43eea078c46cff76b248ccfd..15e915a9c9fec5ee6bf61f72c98969faadda58e2 100644 (file)
@@ -12,6 +12,17 @@ try:
 except ImportError:
     from async_generator import asynccontextmanager  # type: ignore # noqa
 
+# Brotli support is optional
+# The C bindings in `brotli` are recommended for CPython.
+# The CFFI bindings in `brotlicffi` are recommended for PyPy and everything else.
+try:
+    import brotlicffi as brotli
+except ImportError:  # pragma: nocover
+    try:
+        import brotli
+    except ImportError:
+        brotli = None
+
 if sys.version_info >= (3, 10) or (
     sys.version_info >= (3, 7) and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0, 7)
 ):
index 50e6798e9e13f3a44ef8b3a381006a00f3b798b4..5081c86f9a200c3e1889df6bb72a36fe477fb57d 100644 (file)
@@ -8,13 +8,9 @@ import io
 import typing
 import zlib
 
+from ._compat import brotli
 from ._exceptions import DecodingError
 
-try:
-    import brotlicffi
-except ImportError:  # pragma: nocover
-    brotlicffi = None
-
 
 class ContentDecoder:
     def decode(self, data: bytes) -> bytes:
@@ -99,18 +95,20 @@ class BrotliDecoder(ContentDecoder):
     """
 
     def __init__(self) -> None:
-        if brotlicffi is None:  # pragma: nocover
+        if brotli is None:  # pragma: nocover
             raise ImportError(
-                "Using 'BrotliDecoder', but the 'brotlicffi' library "
-                "is not installed."
+                "Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' "
+                "packages have been installed. "
                 "Make sure to install httpx using `pip install httpx[brotli]`."
             ) from None
 
-        self.decompressor = brotlicffi.Decompressor()
+        self.decompressor = brotli.Decompressor()
         self.seen_data = False
         if hasattr(self.decompressor, "decompress"):
-            self._decompress = self.decompressor.decompress
+            # The 'brotlicffi' package.
+            self._decompress = self.decompressor.decompress  # pragma: nocover
         else:
+            # The 'brotli' package.
             self._decompress = self.decompressor.process  # pragma: nocover
 
     def decode(self, data: bytes) -> bytes:
@@ -118,8 +116,8 @@ class BrotliDecoder(ContentDecoder):
             return b""
         self.seen_data = True
         try:
-            return self.decompressor.decompress(data)
-        except brotlicffi.Error as exc:
+            return self._decompress(data)
+        except brotli.error as exc:
             raise DecodingError(str(exc)) from exc
 
     def flush(self) -> bytes:
@@ -127,9 +125,14 @@ class BrotliDecoder(ContentDecoder):
             return b""
         try:
             if hasattr(self.decompressor, "finish"):
-                self.decompressor.finish()
+                # Only available in the 'brotlicffi' package.
+
+                # As the decompressor decompresses eagerly, this
+                # will never actually emit any data. However, it will potentially throw
+                # errors if a truncated or damaged data stream has been used.
+                self.decompressor.finish()  # pragma: nocover
             return b""
-        except brotlicffi.Error as exc:  # pragma: nocover
+        except brotli.error as exc:  # pragma: nocover
             raise DecodingError(str(exc)) from exc
 
 
@@ -326,5 +329,5 @@ SUPPORTED_DECODERS = {
 }
 
 
-if brotlicffi is None:
+if brotli is None:
     SUPPORTED_DECODERS.pop("br")  # pragma: nocover
index aeee7c6c18d599faedf7b0568f3b7e2635bfd6b4..243bbd830bf0fd0fa4993a8878f894613deb992b 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -65,7 +65,10 @@ setup(
     ],
     extras_require={
         "http2": "h2>=3,<5",
-        "brotli": "brotlicffi==1.*",
+        "brotli": [
+            "brotli; platform_python_implementation == 'CPython'",
+            "brotlicffi; platform_python_implementation != 'CPython'"
+        ],
     },
     classifiers=[
         "Development Status :: 4 - Beta",
index b9cc077320f3aa2c9dd3b10dd8b9687aa526e047..adcf2f65c7acd9817a8dfd3c17c4a2298ba5caa6 100644 (file)
@@ -1,10 +1,10 @@
 import json
 import pickle
 
-import brotlicffi
 import pytest
 
 import httpx
+from httpx._compat import brotli
 
 
 class StreamingBody:
@@ -798,7 +798,7 @@ def test_link_headers(headers, expected):
 def test_decode_error_with_request(header_value):
     headers = [(b"Content-Encoding", header_value)]
     body = b"test 123"
-    compressed_body = brotlicffi.compress(body)[3:]
+    compressed_body = brotli.compress(body)[3:]
     with pytest.raises(httpx.DecodingError):
         httpx.Response(
             200,
@@ -819,7 +819,7 @@ def test_decode_error_with_request(header_value):
 def test_value_error_without_request(header_value):
     headers = [(b"Content-Encoding", header_value)]
     body = b"test 123"
-    compressed_body = brotlicffi.compress(body)[3:]
+    compressed_body = brotli.compress(body)[3:]
     with pytest.raises(httpx.DecodingError):
         httpx.Response(200, headers=headers, content=compressed_body)
 
index 4f182efdcc2697ecff2b50b6e2a217737e1b52ba..f31abf098b45de77dd951af89c27ec57c9200392 100644 (file)
@@ -1,9 +1,9 @@
 import zlib
 
-import brotlicffi
 import pytest
 
 import httpx
+from httpx._compat import brotli
 from httpx._decoders import (
     BrotliDecoder,
     ByteChunker,
@@ -69,7 +69,7 @@ def test_gzip():
 
 def test_brotli():
     body = b"test 123"
-    compressed_body = brotlicffi.compress(body)
+    compressed_body = brotli.compress(body)
 
     headers = [(b"Content-Encoding", b"br")]
     response = httpx.Response(
@@ -102,7 +102,7 @@ def test_multi():
 
 def test_multi_with_identity():
     body = b"test 123"
-    compressed_body = brotlicffi.compress(body)
+    compressed_body = brotli.compress(body)
 
     headers = [(b"Content-Encoding", b"br, identity")]
     response = httpx.Response(
@@ -165,7 +165,7 @@ def test_decoders_empty_cases(decoder):
 def test_decoding_errors(header_value):
     headers = [(b"Content-Encoding", header_value)]
     body = b"test 123"
-    compressed_body = brotlicffi.compress(body)[3:]
+    compressed_body = brotli.compress(body)[3:]
     with pytest.raises(httpx.DecodingError):
         request = httpx.Request("GET", "https://example.org")
         httpx.Response(200, headers=headers, content=compressed_body, request=request)