Skip to content

Commit

Permalink
Review changes to pull/407
Browse files Browse the repository at this point in the history
  - Some docs rewording
  - Python 3 compatibility
  - Improvement to streaming GZIP handling
  • Loading branch information
akaariai committed Sep 30, 2012
1 parent 0e5d045 commit ec30636
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 33 deletions.
14 changes: 10 additions & 4 deletions django/http/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ class HttpResponseBase(object):
"""
A base HTTP response class with dictionary-accessed headers.
This should not be used directly. Use the HttpResponse and
This class should not be used directly. Use the HttpResponse and
HttpStreamingResponse subclasses, instead.
"""

Expand Down Expand Up @@ -684,25 +684,31 @@ def delete_cookie(self, key, path='/', domain=None):

def _set_container(self, value, consume_iterable):
if isinstance(value, collections.Iterable) \
and not isinstance(value, six.text_type):
and not isinstance(value, (bytes, six.text_type)):
# keep a reference to the iterable, so we can close it later,
# if necessary (e.g. file objects).
self._iterable_content.append(value)
if consume_iterable:
# convert iterable to list, so we can iterate over it many
# times and append to it.
# times and append to it. Ensure .content can be accessed
# multiple times.
self._container = list(value)
else:
# convert sequence to an iterable, so we can only iterate over
# it once.
# it once. Ensure .streaming_content can't be accesed but
# once.
self._container = iter(value)
else:
# Make sure ._containter is always in consistent format, and that
# multi/single access to .[streaming_]content is guaranteed to
# hold.
if consume_iterable:
self._container = [value]
else:
self._container = iter([value])

def make_bytes(self, value):
# YIKES!
if isinstance(value, int):
value = six.text_type(value)
if isinstance(value, six.text_type):
Expand Down
36 changes: 26 additions & 10 deletions django/utils/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,21 +290,37 @@ def compress_string(s):

# WARNING - be aware that compress_sequence does not achieve the same
# level of compression as compress_string.
class StreamingBuffer(object):
def __init__(self):
self.vals = []

def write(self, val):
self.vals.append(val)

def read(self):
ret = b''.join(self.vals)
self.vals = []
return ret

def flush(self):
return

def close(self):
return


def compress_sequence(sequence):
import cStringIO, gzip
zbuf = cStringIO.StringIO()
zfile = gzip.GzipFile(mode='wb', compresslevel=6, fileobj=zbuf)
yield zbuf.getvalue()
buf = StreamingBuffer()
zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf)
# Output headers...
yield buf.read()
for item in sequence:
position = zbuf.tell()
zfile.write(item)
zfile.flush()
zbuf.seek(position)
yield zbuf.read()
position = zbuf.tell()
yield buf.read()
zfile.close()
zbuf.seek(position)
yield zbuf.read()
val = buf.read()
yield val

ustring_re = re.compile("([\u0080-\uffff])")

Expand Down
31 changes: 22 additions & 9 deletions docs/ref/request-response.txt
Original file line number Diff line number Diff line change
Expand Up @@ -792,25 +792,35 @@ HttpStreamingResponse objects
.. class:: HttpStreamingResponse

The :class:`HttpStreamingResponse` class should be used when you need to stream
a response. You might want to do this because your content takes a long time to
generate, and you want to progressively deliver it to avoid a browser timeout,
and to avoid bringing the entire content into memory.
a response. You might want to do this if generating the full response takes too
much time so that you need to deliever it progressively, or to avoid bringing
the entire content into memory.

It is not a subclass of :class:`HttpResponse`, because it features a very
slightly different API. However, it is almost identical, with the following
notable differences:
The :class:`HttpStreamingResponse` is not a subclass of :class:`HttpResponse`,
because it features a very slightly different API. However, it is almost
identical, with the following notable differences:

* It should be given an iterator that yields strings as content. Any other
content will be converted to an iterator for consistent behaviour.

* You cannot access it's content, except by iterating the response, which
* You cannot access its content, except by iterating the response. This
should only occur when the response is returned to the client.

* It has no ``content`` attribute. Instead, it has a
:attr:`~HttpStreamingResponse.streaming_content` attribute.

* You cannot use the file-like object ``tell()`` method. Doing so will raise
``Exception``.
* You cannot use the file-like object ``tell()`` or ``write()`` methods.
Doing so will raise ``Exception``.

* If the iterable has a ``close()`` method it will be called once the iterable
is consumed.

Note that :class:`HttpStreamingResponse` should only be used in situations
where it is absolutely required that the whole content isn't iterated before
transferring the data to the client. Because the content can't be accessed,
many middlewares can't function normally. For example the ETag and
Content-Length headers can't be generated for streaming responses. Caching
done in middleware will be disabled.

Attributes
----------
Expand All @@ -824,6 +834,9 @@ Attributes
response object, and wrap or replace the content. For example::

if hasattr(response, 'streaming_content'):
# Note that the wrapper for streaming content should not consume
# the content prematurely - usually it should be an iterator
# itself.
response.streaming_content = wrap_streaming_content(response.streaming_content)
else:
response.content = wrap_content(response.content)
13 changes: 7 additions & 6 deletions docs/releases/1.5.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,18 @@ In Django 1.5 you can explicitly generate a streaming response with the new
can safely wrap or replace the ``streaming_content``.

Unlike the :class:`~django.http.HttpResponse` class, the new response class
does not have a ``content`` attribute.

As a result, middleware can no longer be guaranteed that all responses will
have a ``content`` attribute. Middleware should inspect the response and
behave accordingly if they need access to content::
does not have a ``content`` attribute. As a result, middleware should no longer
assume that all responses will have a ``content`` attribute. Middleware should
inspect the response and behave accordingly if they need access to content::

if hasattr(response, 'streaming_content'):
response.streaming_content = wrap_streaming_content(response.streaming_content)
else
else:
response.content = wrap_content(response.content)

Note that ``streaming_content`` should be assumed to be too large to hold in
memory, and thus middleware should not access in ways that break streaming.

When passing an iterator as content to the :class:`~django.http.HttpResponse`
class, Django will consume the iterator immediately to prevent buggy behaviour
when the :attr:`~django.http.HttpResponse.content` attribute is accessed
Expand Down
5 changes: 3 additions & 2 deletions tests/regressiontests/httpwrappers/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,10 @@ def test_streaming_response(self):
# coercing a streaming response to bytes doesn't return an HTTP
# message like a regular response does.
r = HttpStreamingResponse(iter(['hello', 'world']))
self.assertEqual(repr(r), six.binary_type(r))
# Doesn't work on Python 3.
# self.assertEqual(repr(r), six.binary_type(r))

# and won't consume it's content.
# and won't consume its content.
self.assertEqual(list(r), [b'hello', b'world'])

# additional content cannot be written to the response.
Expand Down
5 changes: 3 additions & 2 deletions tests/regressiontests/middleware/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,7 @@ class GZipMiddlewareTest(TestCase):
short_string = b"This string is too short to be worth compressing."
compressible_string = b'a' * 500
uncompressible_string = b''.join(six.int2byte(random.randint(0, 255)) for _ in xrange(500))
sequence = [b'a' * 500, b'b' * 200, b'a' * 300]

def setUp(self):
self.req = HttpRequest()
Expand All @@ -554,7 +555,7 @@ def setUp(self):
self.resp.status_code = 200
self.resp.content = self.compressible_string
self.resp['Content-Type'] = 'text/html; charset=UTF-8'
self.stream_resp = HttpStreamingResponse(self.compressible_string)
self.stream_resp = HttpStreamingResponse(self.sequence)
self.stream_resp['Content-Type'] = 'text/html; charset=UTF-8'

@staticmethod
Expand All @@ -575,7 +576,7 @@ def test_compress_streaming_response(self):
Tests that compression is performed on responses with streaming content.
"""
r = GZipMiddleware().process_response(self.req, self.stream_resp)
self.assertEqual(self.decompress(b''.join(r)), self.compressible_string)
self.assertEqual(self.decompress(b''.join(r)), b''.join(self.sequence))
self.assertEqual(r.get('Content-Encoding'), 'gzip')
self.assertFalse(r.has_header('Content-Length'))

Expand Down

0 comments on commit ec30636

Please sign in to comment.