Skip to content

Commit 9d94e14

Browse files
committed
Enable brotli decompression if it is available
1 parent 8c0bb73 commit 9d94e14

File tree

3 files changed

+43
-6
lines changed

3 files changed

+43
-6
lines changed

tests/integration/test_filter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from urllib.parse import urlencode
55
from urllib.error import HTTPError
66
import vcr
7+
from vcr.filters import brotli
78
import json
89
from assertions import assert_cassette_has_one_response, assert_is_json
910

@@ -118,6 +119,22 @@ def test_decompress_deflate(tmpdir, httpbin):
118119
assert_is_json(decoded_response)
119120

120121

122+
def test_decompress_brotli(tmpdir, httpbin):
123+
if brotli is None:
124+
# XXX: this is never true, because brotlipy is installed with "httpbin"
125+
pytest.skip('Brotli is not installed')
126+
127+
url = httpbin.url + "/brotli"
128+
request = Request(url, headers={"Accept-Encoding": ["gzip, deflate, br"]})
129+
cass_file = str(tmpdir.join("brotli_response.yaml"))
130+
with vcr.use_cassette(cass_file, decode_compressed_response=True):
131+
urlopen(request)
132+
with vcr.use_cassette(cass_file) as cass:
133+
decoded_response = urlopen(url).read()
134+
assert_cassette_has_one_response(cass)
135+
assert_is_json(decoded_response)
136+
137+
121138
def test_decompress_regular(tmpdir, httpbin):
122139
"""Test that it doesn't try to decompress content that isn't compressed"""
123140
url = httpbin.url + "/get"

tox.ini

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ skip_missing_interpreters=true
33
envlist =
44
cov-clean,
55
lint,
6-
{py36,py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx},
7-
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3},
6+
{py36,py37,py38,py39,py310}-{requests,httplib2,urllib3,tornado4,boto3,aiohttp,httpx,brotli,brotlipy,brotlicffi},
7+
{pypy3}-{requests,httplib2,urllib3,tornado4,boto3,brotli,brotlipy,brotlicffi},
88
cov-report
99

1010

@@ -91,6 +91,10 @@ deps =
9191
httpx: httpx
9292
{py36,py37,py38,py39,py310}-{httpx}: httpx
9393
{py36,py37,py38,py39,py310}-{httpx}: pytest-asyncio
94+
{py36,py37,py38,py39,py310}-{httpx}: httpx
95+
brotli: brotli
96+
brotlipy: brotlipy
97+
brotlicffi: brotlicffi
9498
depends =
9599
lint,{py36,py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py36,py37,py38,py39,py310}-{aiohttp},{py36,py37,py38,py39,py310}-{httpx}: cov-clean
96100
cov-report: lint,{py36,py37,py38,py39,py310,pypy3}-{requests,httplib2,urllib3,tornado4,boto3},{py36,py37,py38,py39,py310}-{aiohttp}

vcr/filters.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66

77
from .util import CaseInsensitiveDict
88

9+
try:
10+
# This supports both brotli & brotlipy packages
11+
import brotli
12+
except ImportError:
13+
try:
14+
import brotlicffi as brotli
15+
except ImportError:
16+
brotli = None
17+
18+
19+
AVAILABLE_DECOMPRESSORS = {'gzip', 'deflate'}
20+
if brotli is not None:
21+
AVAILABLE_DECOMPRESSORS.add('br')
22+
923

1024
def replace_headers(request, replacements):
1125
"""Replace headers in request according to replacements.
@@ -142,24 +156,26 @@ def decode_response(response):
142156
3. update content-length header to decompressed length
143157
"""
144158

145-
def is_compressed(headers):
159+
def is_decompressable(headers):
146160
encoding = headers.get("content-encoding", [])
147-
return encoding and encoding[0] in ("gzip", "deflate")
161+
return encoding and encoding[0] in AVAILABLE_DECOMPRESSORS
148162

149163
def decompress_body(body, encoding):
150164
"""Returns decompressed body according to encoding using zlib.
151165
to (de-)compress gzip format, use wbits = zlib.MAX_WBITS | 16
152166
"""
153167
if encoding == "gzip":
154168
return zlib.decompress(body, zlib.MAX_WBITS | 16)
155-
else: # encoding == 'deflate'
169+
elif encoding == "deflate":
156170
return zlib.decompress(body)
171+
else: # encoding == 'br'
172+
return brotli.decompress(body)
157173

158174
# Deepcopy here in case `headers` contain objects that could
159175
# be mutated by a shallow copy and corrupt the real response.
160176
response = copy.deepcopy(response)
161177
headers = CaseInsensitiveDict(response["headers"])
162-
if is_compressed(headers):
178+
if is_decompressable(headers):
163179
encoding = headers["content-encoding"][0]
164180
headers["content-encoding"].remove(encoding)
165181
if not headers["content-encoding"]:

0 commit comments

Comments
 (0)