Skip to content

Commit fb3938a

Browse files
committed
Merge pull request #403 from danielnelson/add-encoding-to-enable-compression
Allow gzip compression in high-level server response interface
2 parents d1b7e1c + de9a292 commit fb3938a

File tree

4 files changed

+128
-16
lines changed

4 files changed

+128
-16
lines changed

aiohttp/web_reqrep.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__all__ = ('Request', 'StreamResponse', 'Response')
1+
__all__ = ('ContentCoding', 'Request', 'StreamResponse', 'Response')
22

33
import asyncio
44
import binascii
@@ -12,6 +12,11 @@
1212
import time
1313
import warnings
1414

15+
try:
16+
import enum
17+
except ImportError:
18+
from flufl import enum
19+
1520
from email.utils import parsedate
1621
from types import MappingProxyType
1722
from urllib.parse import urlsplit, parse_qsl, unquote
@@ -72,6 +77,16 @@ def content_length(self, _CONTENT_LENGTH=hdrs.CONTENT_LENGTH):
7277
FileField = collections.namedtuple('Field', 'name filename file content_type')
7378

7479

80+
class ContentCoding(enum.Enum):
81+
# The content codings that we have support for.
82+
#
83+
# Additional registered codings are listed at:
84+
# https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
85+
deflate = 'deflate'
86+
gzip = 'gzip'
87+
identity = 'identity'
88+
89+
7590
############################################################
7691
# HTTP Request
7792
############################################################
@@ -436,8 +451,12 @@ def enable_chunked_encoding(self, chunk_size=None):
436451
self._chunked = True
437452
self._chunk_size = chunk_size
438453

439-
def enable_compression(self, force=False):
440-
"""Enables response compression with `deflate` encoding."""
454+
def enable_compression(self, force=None):
455+
"""Enables response compression encoding."""
456+
# Backwards compatibility for when force was a bool <0.17.
457+
if type(force) == bool:
458+
force = ContentCoding.deflate if force else ContentCoding.identity
459+
441460
self._compression = True
442461
self._compression_force = force
443462

@@ -577,6 +596,22 @@ def _start_pre_check(self, request):
577596
else:
578597
return None
579598

599+
def _start_compression(self, request):
600+
def start(coding):
601+
if coding != ContentCoding.identity:
602+
self.headers[hdrs.CONTENT_ENCODING] = coding.value
603+
self._resp_impl.add_compression_filter(coding.value)
604+
605+
if self._compression_force:
606+
start(self._compression_force)
607+
else:
608+
accept_encoding = request.headers.get(
609+
hdrs.ACCEPT_ENCODING, '').lower()
610+
for coding in ContentCoding:
611+
if coding.value in accept_encoding:
612+
start(coding)
613+
return
614+
580615
def start(self, request):
581616
resp_impl = self._start_pre_check(request)
582617
if resp_impl is not None:
@@ -598,10 +633,7 @@ def start(self, request):
598633
self._copy_cookies()
599634

600635
if self._compression:
601-
if (self._compression_force or
602-
'deflate' in request.headers.get(
603-
hdrs.ACCEPT_ENCODING, '')):
604-
resp_impl.add_compression_filter()
636+
self._start_compression(request)
605637

606638
if self._chunked:
607639
resp_impl.enable_chunked_encoding()

docs/web_reference.rst

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,14 +382,15 @@ StreamResponse
382382

383383
.. seealso:: :meth:`enable_compression`
384384

385-
.. method:: enable_compression(force=False)
385+
.. method:: enable_compression(force=None)
386386

387387
Enable compression.
388388

389-
When *force* is ``False`` (default) compression is used only
390-
when *deflate* is in *Accept-Encoding* request's header.
389+
When *force* is unset compression encoding is selected based on
390+
the request's *Accept-Encoding* header.
391391

392-
*Accept-Encoding* is not checked if *force* is ``True``.
392+
*Accept-Encoding* is not checked if *force* is set to a
393+
:class:`ContentCoding`.
393394

394395
.. versionadded:: 0.14
395396

@@ -1217,3 +1218,17 @@ Utilities
12171218
*MIME type* of uploaded file, ``'text/plain'`` by default.
12181219

12191220
.. seealso:: :ref:`aiohttp-web-file-upload`
1221+
1222+
1223+
Constants
1224+
---------
1225+
1226+
.. class:: ContentCoding
1227+
1228+
An :class:`enum.Enum` class of available Content Codings.
1229+
1230+
.. attribute:: deflate
1231+
1232+
.. attribute:: gzip
1233+
1234+
.. attribute:: identity

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def build_extension(self, ext):
5656
install_requires = ['chardet']
5757

5858
if sys.version_info < (3, 4):
59-
install_requires += ['asyncio']
59+
install_requires += ['asyncio', 'flufl.enum']
6060

6161
tests_require = install_requires + ['nose', 'gunicorn']
6262

tests/test_web_response.py

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from unittest import mock
55
from aiohttp import hdrs
66
from aiohttp.multidict import CIMultiDict
7-
from aiohttp.web import Request, StreamResponse, Response
7+
from aiohttp.web import ContentCoding, Request, StreamResponse, Response
88
from aiohttp.protocol import HttpVersion, HttpVersion11, HttpVersion10
99
from aiohttp.protocol import RawRequestMessage
1010

@@ -197,7 +197,7 @@ def test_compression_no_accept(self, ResponseImpl):
197197
self.assertFalse(msg.add_compression_filter.called)
198198

199199
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
200-
def test_force_compression_no_accept(self, ResponseImpl):
200+
def test_force_compression_no_accept_backwards_compat(self, ResponseImpl):
201201
req = self.make_request('GET', '/')
202202
resp = StreamResponse()
203203
self.assertFalse(resp.chunked)
@@ -211,7 +211,19 @@ def test_force_compression_no_accept(self, ResponseImpl):
211211
self.assertIsNotNone(msg.filter)
212212

213213
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
214-
def test_compression(self, ResponseImpl):
214+
def test_force_compression_false_backwards_compat(self, ResponseImpl):
215+
req = self.make_request('GET', '/')
216+
resp = StreamResponse()
217+
218+
self.assertFalse(resp.compression)
219+
resp.enable_compression(force=False)
220+
self.assertTrue(resp.compression)
221+
222+
msg = resp.start(req)
223+
self.assertFalse(msg.add_compression_filter.called)
224+
225+
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
226+
def test_compression_default_coding(self, ResponseImpl):
215227
req = self.make_request(
216228
'GET', '/',
217229
headers=CIMultiDict({hdrs.ACCEPT_ENCODING: 'gzip, deflate'}))
@@ -223,9 +235,62 @@ def test_compression(self, ResponseImpl):
223235
self.assertTrue(resp.compression)
224236

225237
msg = resp.start(req)
226-
self.assertTrue(msg.add_compression_filter.called)
238+
msg.add_compression_filter.assert_called_with('deflate')
239+
self.assertEqual('deflate', resp.headers.get(hdrs.CONTENT_ENCODING))
227240
self.assertIsNotNone(msg.filter)
228241

242+
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
243+
def test_force_compression_deflate(self, ResponseImpl):
244+
req = self.make_request(
245+
'GET', '/',
246+
headers=CIMultiDict({hdrs.ACCEPT_ENCODING: 'gzip, deflate'}))
247+
resp = StreamResponse()
248+
249+
resp.enable_compression(ContentCoding.deflate)
250+
self.assertTrue(resp.compression)
251+
252+
msg = resp.start(req)
253+
msg.add_compression_filter.assert_called_with('deflate')
254+
self.assertEqual('deflate', resp.headers.get(hdrs.CONTENT_ENCODING))
255+
256+
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
257+
def test_force_compression_no_accept_deflate(self, ResponseImpl):
258+
req = self.make_request('GET', '/')
259+
resp = StreamResponse()
260+
261+
resp.enable_compression(ContentCoding.deflate)
262+
self.assertTrue(resp.compression)
263+
264+
msg = resp.start(req)
265+
msg.add_compression_filter.assert_called_with('deflate')
266+
self.assertEqual('deflate', resp.headers.get(hdrs.CONTENT_ENCODING))
267+
268+
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
269+
def test_force_compression_gzip(self, ResponseImpl):
270+
req = self.make_request(
271+
'GET', '/',
272+
headers=CIMultiDict({hdrs.ACCEPT_ENCODING: 'gzip, deflate'}))
273+
resp = StreamResponse()
274+
275+
resp.enable_compression(ContentCoding.gzip)
276+
self.assertTrue(resp.compression)
277+
278+
msg = resp.start(req)
279+
msg.add_compression_filter.assert_called_with('gzip')
280+
self.assertEqual('gzip', resp.headers.get(hdrs.CONTENT_ENCODING))
281+
282+
@mock.patch('aiohttp.web_reqrep.ResponseImpl')
283+
def test_force_compression_no_accept_gzip(self, ResponseImpl):
284+
req = self.make_request('GET', '/')
285+
resp = StreamResponse()
286+
287+
resp.enable_compression(ContentCoding.gzip)
288+
self.assertTrue(resp.compression)
289+
290+
msg = resp.start(req)
291+
msg.add_compression_filter.assert_called_with('gzip')
292+
self.assertEqual('gzip', resp.headers.get(hdrs.CONTENT_ENCODING))
293+
229294
def test_write_non_byteish(self):
230295
resp = StreamResponse()
231296
resp.start(self.make_request('GET', '/'))

0 commit comments

Comments
 (0)