Ticket #7581: 7581-20121020.patch

File 7581-20121020.patch, 35.9 KB (added by Aymeric Augustin, 12 years ago)
  • django/http/__init__.py

    diff --git a/django/http/__init__.py b/django/http/__init__.py
    index b385b45..11cd5e9 100644
    a b def parse_cookie(cookie):  
    526526class BadHeaderError(ValueError):
    527527    pass
    528528
    529 class HttpResponse(object):
    530     """A basic HTTP response, with content and dictionary-accessed headers."""
     529class HttpResponseBase(object):
     530    """
     531    An HTTP response base class with dictionary-accessed headers.
     532
     533    This class doesn't handle content. It should not be used directly.
     534    Use the HttpResponse and StreamingHttpResponse subclasses instead.
     535    """
    531536
    532537    status_code = 200
    533538
    534     def __init__(self, content='', content_type=None, status=None,
    535             mimetype=None):
     539    def __init__(self, content_type=None, status=None, mimetype=None):
    536540        # _headers is a mapping of the lower-case name to the original case of
    537541        # the header (required for working with legacy systems) and the header
    538542        # value. Both the name of the header and its value are ASCII strings.
    539543        self._headers = {}
    540544        self._charset = settings.DEFAULT_CHARSET
     545        self._closable_objects = []
    541546        if mimetype:
    542547            warnings.warn("Using mimetype keyword argument is deprecated, use"
    543548                          " content_type instead", PendingDeprecationWarning)
    class HttpResponse(object):  
    545550        if not content_type:
    546551            content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
    547552                    self._charset)
    548         # content is a bytestring. See the content property methods.
    549         self.content = content
    550553        self.cookies = SimpleCookie()
    551554        if status:
    552555            self.status_code = status
    553556
    554557        self['Content-Type'] = content_type
    555558
    556     def serialize(self):
    557         """Full HTTP message, including headers, as a bytestring."""
     559    def serialize_headers(self):
     560        """HTTP headers as a bytestring."""
    558561        headers = [
    559562            ('%s: %s' % (key, value)).encode('us-ascii')
    560563            for key, value in self._headers.values()
    561564        ]
    562         return b'\r\n'.join(headers) + b'\r\n\r\n' + self.content
     565        return b'\r\n'.join(headers)
    563566
    564567    if six.PY3:
    565         __bytes__ = serialize
     568        __bytes__ = serialize_headers
    566569    else:
    567         __str__ = serialize
     570        __str__ = serialize_headers
    568571
    569572    def _convert_to_charset(self, value, charset, mime_encode=False):
    570573        """Converts headers key/value to ascii/latin1 native strings.
    class HttpResponse(object):  
    688691        self.set_cookie(key, max_age=0, path=path, domain=domain,
    689692                        expires='Thu, 01-Jan-1970 00:00:00 GMT')
    690693
     694    # Common methods used by subclasses
     695
     696    def make_bytes(self, value):
     697        """Turn a value into a bytestring encoded in the output charset."""
     698        # For backwards compatibility, this method supports values that are
     699        # unlikely to occur in real applications. It has grown complex and
     700        # should be refactored. It also overlaps __next__. See #18796.
     701        if self.has_header('Content-Encoding'):
     702            if isinstance(value, int):
     703                value = six.text_type(value)
     704            if isinstance(value, six.text_type):
     705                value = value.encode('ascii')
     706            # force conversion to bytes in case chunk is a subclass
     707            return bytes(value)
     708        else:
     709            return force_bytes(value, self._charset)
     710
     711    # These methods partially implement the file-like object interface.
     712    # See http://docs.python.org/lib/bltin-file-objects.html
     713
     714    def close(self):
     715        for closable in self._closable_objects:
     716            closable.close()
     717
     718    def write(self, content):
     719        raise Exception("This %s instance is not writable" % self.__class__.__name__)
     720
     721    def flush(self):
     722        pass
     723
     724    def tell(self):
     725        raise Exception("This %s instance cannot tell its position" % self.__class__.__name__)
     726
     727class HttpResponse(HttpResponseBase):
     728    """
     729    An HTTP response class with a string as content.
     730
     731    This content that can be read, appended to or replaced.
     732    """
     733
     734    streaming = False
     735
     736    def __init__(self, content='', *args, **kwargs):
     737        super(HttpResponse, self).__init__(*args, **kwargs)
     738        # Content is a bytestring. See the `content` property methods.
     739        self.content = content
     740
     741    def serialize(self):
     742        """Full HTTP message, including headers, as a bytestring."""
     743        return self.serialize_headers() + b'\r\n\r\n' + self.content
     744
     745    if six.PY3:
     746        __bytes__ = serialize
     747    else:
     748        __str__ = serialize
     749
    691750    @property
    692751    def content(self):
    693         if self.has_header('Content-Encoding'):
    694             def make_bytes(value):
    695                 if isinstance(value, int):
    696                     value = six.text_type(value)
    697                 if isinstance(value, six.text_type):
    698                     value = value.encode('ascii')
    699                 # force conversion to bytes in case chunk is a subclass
    700                 return bytes(value)
    701             return b''.join(make_bytes(e) for e in self._container)
    702         return b''.join(force_bytes(e, self._charset) for e in self._container)
     752        return b''.join(self.make_bytes(e) for e in self._container)
    703753
    704754    @content.setter
    705755    def content(self, value):
    706756        if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)):
    707757            self._container = value
    708758            self._base_content_is_iter = True
     759            if hasattr(value, 'close'):
     760                self._closable_objects.append(value)
    709761        else:
    710762            self._container = [value]
    711763            self._base_content_is_iter = False
    class HttpResponse(object):  
    725777
    726778    next = __next__             # Python 2 compatibility
    727779
    728     def close(self):
    729         if hasattr(self._container, 'close'):
    730             self._container.close()
    731 
    732     # The remaining methods partially implement the file-like object interface.
    733     # See http://docs.python.org/lib/bltin-file-objects.html
    734780    def write(self, content):
    735781        if self._base_content_is_iter:
    736             raise Exception("This %s instance is not writable" % self.__class__)
     782            raise Exception("This %s instance is not writable" % self.__class__.__name__)
    737783        self._container.append(content)
    738784
    739     def flush(self):
    740         pass
    741 
    742785    def tell(self):
    743786        if self._base_content_is_iter:
    744             raise Exception("This %s instance cannot tell its position" % self.__class__)
     787            raise Exception("This %s instance cannot tell its position" % self.__class__.__name__)
    745788        return sum([len(chunk) for chunk in self])
    746789
     790class StreamingHttpResponse(HttpResponseBase):
     791    """
     792    A streaming HTTP response class with an iterator as content.
     793
     794    This should only be iterated once, when the response is streamed to the
     795    client. However, it can be appended to or replaced with a new iterator
     796    that wraps the original content (or yields entirely new content).
     797    """
     798
     799    streaming = True
     800
     801    def __init__(self, streaming_content=(), *args, **kwargs):
     802        super(StreamingHttpResponse, self).__init__(*args, **kwargs)
     803        # `streaming_content` should be an iterable of bytestrings.
     804        # See the `streaming_content` property methods.
     805        self.streaming_content = streaming_content
     806
     807    @property
     808    def content(self):
     809        raise AttributeError("This %s instance has no `content` attribute. "
     810            "Use `streaming_content` instead." % self.__class__.__name__)
     811
     812    @property
     813    def streaming_content(self):
     814        return (self.make_bytes(chunk) for chunk in self._iterator)
     815
     816    @streaming_content.setter
     817    def streaming_content(self, value):
     818        # Ensure we can never iterate on "value" more than once.
     819        self._iterator = iter(value)
     820        if hasattr(value, 'close'):
     821            self._closable_objects.append(value)
     822
     823    def __iter__(self):
     824        return self
     825
     826    def __next__(self):
     827        try:
     828            chunk = next(self._iterator)
     829        except StopIteration:
     830            self.close()
     831            raise
     832        return self.make_bytes(chunk)
     833
     834    next = __next__             # Python 2 compatibility
     835
     836class CompatibleStreamingHttpResponse(StreamingHttpResponse):
     837    """
     838    This class maintains compatibility with middleware that doesn't know how
     839    to handle the content of a streaming response by exposing a `content`
     840    attribute that will consume and cache the content iterator when accessed.
     841
     842    These responses will stream only if no middleware attempts to access the
     843    `content` attribute. Otherwise, they will behave like a regular response,
     844    and raise a `PendingDeprecationWarning`.
     845    """
     846    @property
     847    def content(self):
     848        warnings.warn(
     849            'Accessing the `content` attribute on a streaming response is '
     850            'deprecated. Use the `streaming_content` attribute instead.',
     851            PendingDeprecationWarning)
     852        content = b''.join(self)
     853        self.streaming_content = [content]
     854        return content
     855
     856    @content.setter
     857    def content(self, content):
     858        warnings.warn(
     859            'Accessing the `content` attribute on a streaming response is '
     860            'deprecated. Use the `streaming_content` attribute instead.',
     861            PendingDeprecationWarning)
     862        self.streaming_content = [content]
     863
    747864class HttpResponseRedirectBase(HttpResponse):
    748865    allowed_schemes = ['http', 'https', 'ftp']
    749866
  • django/http/utils.py

    diff --git a/django/http/utils.py b/django/http/utils.py
    index 0180864..fe42ecd 100644
    a b def conditional_content_removal(request, response):  
    2626    responses. Ensures compliance with RFC 2616, section 4.3.
    2727    """
    2828    if 100 <= response.status_code < 200 or response.status_code in (204, 304):
    29        response.content = ''
    30        response['Content-Length'] = 0
     29        if response.streaming:
     30            response.streaming_content = []
     31        else:
     32            response.content = ''
     33        response['Content-Length'] = 0
    3134    if request.method == 'HEAD':
    32         response.content = ''
     35        if response.streaming:
     36            response.streaming_content = []
     37        else:
     38            response.content = ''
    3339    return response
    3440
    3541def fix_IE_for_attach(request, response):
  • django/middleware/common.py

    diff --git a/django/middleware/common.py b/django/middleware/common.py
    index 0ec17fb..6fbbf43 100644
    a b class CommonMiddleware(object):  
    113113        if settings.USE_ETAGS:
    114114            if response.has_header('ETag'):
    115115                etag = response['ETag']
     116            elif response.streaming:
     117                etag = None
    116118            else:
    117119                etag = '"%s"' % hashlib.md5(response.content).hexdigest()
    118             if response.status_code >= 200 and response.status_code < 300 and request.META.get('HTTP_IF_NONE_MATCH') == etag:
    119                 cookies = response.cookies
    120                 response = http.HttpResponseNotModified()
    121                 response.cookies = cookies
    122             else:
    123                 response['ETag'] = etag
     120            if etag is not None:
     121                if (200 <= response.status_code < 300
     122                    and request.META.get('HTTP_IF_NONE_MATCH') == etag):
     123                    cookies = response.cookies
     124                    response = http.HttpResponseNotModified()
     125                    response.cookies = cookies
     126                else:
     127                    response['ETag'] = etag
    124128
    125129        return response
    126130
  • django/middleware/gzip.py

    diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py
    index 69f938c..fb54501 100644
    a b  
    11import re
    22
    3 from django.utils.text import compress_string
     3from django.utils.text import compress_sequence, compress_string
    44from django.utils.cache import patch_vary_headers
    55
    66re_accepts_gzip = re.compile(r'\bgzip\b')
    class GZipMiddleware(object):  
    1313    """
    1414    def process_response(self, request, response):
    1515        # It's not worth attempting to compress really short responses.
    16         if len(response.content) < 200:
     16        if not response.streaming and len(response.content) < 200:
    1717            return response
    1818
    1919        patch_vary_headers(response, ('Accept-Encoding',))
    class GZipMiddleware(object):  
    3232        if not re_accepts_gzip.search(ae):
    3333            return response
    3434
    35         # Return the compressed content only if it's actually shorter.
    36         compressed_content = compress_string(response.content)
    37         if len(compressed_content) >= len(response.content):
    38             return response
     35        if response.streaming:
     36            # Delete the `Content-Length` header for streaming content, because
     37            # we won't know the compressed size until we stream it.
     38            response.streaming_content = compress_sequence(response.streaming_content)
     39            del response['Content-Length']
     40        else:
     41            # Return the compressed content only if it's actually shorter.
     42            compressed_content = compress_string(response.content)
     43            if len(compressed_content) >= len(response.content):
     44                return response
     45            response.content = compressed_content
     46            response['Content-Length'] = str(len(response.content))
    3947
    4048        if response.has_header('ETag'):
    4149            response['ETag'] = re.sub('"$', ';gzip"', response['ETag'])
    42 
    43         response.content = compressed_content
    4450        response['Content-Encoding'] = 'gzip'
    45         response['Content-Length'] = str(len(response.content))
     51
    4652        return response
  • django/middleware/http.py

    diff --git a/django/middleware/http.py b/django/middleware/http.py
    index 86e46ce..5a46e04 100644
    a b class ConditionalGetMiddleware(object):  
    1010    """
    1111    def process_response(self, request, response):
    1212        response['Date'] = http_date()
    13         if not response.has_header('Content-Length'):
     13        if not response.streaming and not response.has_header('Content-Length'):
    1414            response['Content-Length'] = str(len(response.content))
    1515
    1616        if response.has_header('ETag'):
  • django/test/testcases.py

    diff --git a/django/test/testcases.py b/django/test/testcases.py
    index 1d52fed..4c5eb5d 100644
    a b class TransactionTestCase(SimpleTestCase):  
    596596            msg_prefix + "Couldn't retrieve content: Response code was %d"
    597597            " (expected %d)" % (response.status_code, status_code))
    598598        text = force_text(text, encoding=response._charset)
    599         content = response.content.decode(response._charset)
     599        if response.streaming:
     600            content = b''.join(response.streaming_content)
     601        else:
     602            content = response.content
     603        content = content.decode(response._charset)
    600604        if html:
    601605            content = assert_and_parse_html(self, content, None,
    602606                "Response's content is not valid HTML:")
  • django/utils/cache.py

    diff --git a/django/utils/cache.py b/django/utils/cache.py
    index 91c4796..0fceaa9 100644
    a b def get_max_age(response):  
    9595            pass
    9696
    9797def _set_response_etag(response):
    98     response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest()
     98    if not response.streaming:
     99        response['ETag'] = '"%s"' % hashlib.md5(response.content).hexdigest()
    99100    return response
    100101
    101102def patch_response_headers(response, cache_timeout=None):
  • django/utils/text.py

    diff --git a/django/utils/text.py b/django/utils/text.py
    index c197084..d75ca8d 100644
    a b def compress_string(s):  
    288288    zfile.close()
    289289    return zbuf.getvalue()
    290290
     291class StreamingBuffer(object):
     292    def __init__(self):
     293        self.vals = []
     294
     295    def write(self, val):
     296        self.vals.append(val)
     297
     298    def read(self):
     299        ret = b''.join(self.vals)
     300        self.vals = []
     301        return ret
     302
     303    def flush(self):
     304        return
     305
     306    def close(self):
     307        return
     308
     309# Like compress_string, but for iterators of strings.
     310def compress_sequence(sequence):
     311    buf = StreamingBuffer()
     312    zfile = GzipFile(mode='wb', compresslevel=6, fileobj=buf)
     313    # Output headers...
     314    yield buf.read()
     315    for item in sequence:
     316        zfile.write(item)
     317        zfile.flush()
     318        yield buf.read()
     319    zfile.close()
     320    yield buf.read()
     321
    291322ustring_re = re.compile("([\u0080-\uffff])")
    292323
    293324def javascript_quote(s, quote_double_quotes=False):
  • django/views/static.py

    diff --git a/django/views/static.py b/django/views/static.py
    index 7dd44c5..f61ba28 100644
    a b try:  
    1414except ImportError:     # Python 2
    1515    from urllib import unquote
    1616
    17 from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified
     17from django.http import (CompatibleStreamingHttpResponse, Http404,
     18    HttpResponse, HttpResponseRedirect, HttpResponseNotModified)
    1819from django.template import loader, Template, Context, TemplateDoesNotExist
    1920from django.utils.http import http_date, parse_http_date
    2021from django.utils.translation import ugettext as _, ugettext_noop
    def serve(request, path, document_root=None, show_indexes=False):  
    6263    if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
    6364                              statobj.st_mtime, statobj.st_size):
    6465        return HttpResponseNotModified()
    65     with open(fullpath, 'rb') as f:
    66         response = HttpResponse(f.read(), content_type=mimetype)
     66    response = CompatibleStreamingHttpResponse(open(fullpath, 'rb'), content_type=mimetype)
    6767    response["Last-Modified"] = http_date(statobj.st_mtime)
    6868    if stat.S_ISREG(statobj.st_mode):
    6969        response["Content-Length"] = statobj.st_size
  • docs/ref/request-response.txt

    diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
    index 0a337eb..e95b13a 100644
    a b file-like object::  
    560560Passing iterators
    561561~~~~~~~~~~~~~~~~~
    562562
    563 Finally, you can pass ``HttpResponse`` an iterator rather than passing it
    564 hard-coded strings. If you use this technique, follow these guidelines:
     563Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you
     564use this technique, the iterator should return strings.
    565565
    566 * The iterator should return strings.
    567 * If an :class:`HttpResponse` has been initialized with an iterator as its
    568   content, you can't use the :class:`HttpResponse` instance as a file-like
    569   object. Doing so will raise ``Exception``.
     566.. versionchanged:: 1.5
     567
     568    Passing an iterator as content to :class:`HttpResponse` creates a
     569    streaming response if (and only if) no middleware accesses the
     570    :attr:`HttpResponse.content` attribute before the response is returned.
     571
     572    If you want to guarantee that your response will stream to the client, you
     573    should use the new :class:`StreamingHttpResponse` class instead.
     574
     575If an :class:`HttpResponse` instance has been initialized with an iterator as
     576its content, you can't use it as a file-like object. Doing so will raise an
     577exception.
    570578
    571579Setting headers
    572580~~~~~~~~~~~~~~~
    Attributes  
    608616
    609617    The `HTTP Status code`_ for the response.
    610618
     619.. attribute:: HttpResponse.streaming
     620
     621    This is always ``False``.
     622
     623    This attribute exists so middleware can treat streaming responses
     624    differently from regular responses.
     625
    611626Methods
    612627-------
    613628
    types of HTTP responses. Like ``HttpResponse``, these subclasses live in  
    775790    method, Django will treat it as emulating a
    776791    :class:`~django.template.response.SimpleTemplateResponse`, and the
    777792    ``render`` method must itself return a valid response object.
     793
     794StreamingHttpResponse objects
     795=============================
     796
     797.. versionadded:: 1.5
     798
     799.. class:: StreamingHttpResponse
     800
     801The :class:`StreamingHttpResponse` class is used to stream a response from
     802Django to the browser. You might want to do this if generating the response
     803takes too long or uses too much memory. For instance, it's useful for
     804generating large CSV files.
     805
     806.. admonition:: Performance considerations
     807
     808    Django is designed for short-lived requests. Streaming responses will tie
     809    a worker process and keep a database connection idle in transaction for
     810    the entire duration of the response. This may result in poor performance.
     811
     812    Generally speaking, you should perform expensive tasks outside of the
     813    request-response cycle, rather than resorting to a streamed response.
     814
     815The :class:`StreamingHttpResponse` is not a subclass of :class:`HttpResponse`,
     816because it features a slightly different API. However, it is almost identical,
     817with the following notable differences:
     818
     819* It should be given an iterator that yields strings as content.
     820
     821* You cannot access its content, except by iterating the response object
     822  itself. This should only occur when the response is returned to the client.
     823
     824* It has no ``content`` attribute. Instead, it has a
     825  :attr:`~StreamingHttpResponse.streaming_content` attribute.
     826
     827* You cannot use the file-like object ``tell()`` or ``write()`` methods.
     828  Doing so will raise an exception.
     829
     830* Any iterators that have a ``close()`` method and are assigned as content will
     831  be closed automatically after the response has been iterated.
     832
     833:class:`StreamingHttpResponse` should only be used in situations where it is
     834absolutely required that the whole content isn't iterated before transferring
     835the data to the client. Because the content can't be accessed, many
     836middlewares can't function normally. For example the ``ETag`` and ``Content-
     837Length`` headers can't be generated for streaming responses.
     838
     839Attributes
     840----------
     841
     842.. attribute:: StreamingHttpResponse.streaming_content
     843
     844    An iterator of strings representing the content.
     845
     846.. attribute:: HttpResponse.status_code
     847
     848    The `HTTP Status code`_ for the response.
     849
     850.. attribute:: HttpResponse.streaming
     851
     852    This is always ``True``.
  • docs/releases/1.5.txt

    diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
    index d49bae8..bec9d96 100644
    a b For one-to-one relationships, both sides can be cached. For many-to-one  
    8484relationships, only the single side of the relationship can be cached. This
    8585is particularly helpful in combination with ``prefetch_related``.
    8686
     87Explicit support for streaming responses
     88~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     89
     90Before Django 1.5, it was possible to create a streaming response by passing
     91an iterator to :class:`~django.http.HttpResponse`. But this was unreliable:
     92any middleware that accessed the :attr:`~django.http.HttpResponse.content`
     93attribute would consume the iterator prematurely.
     94
     95You can now explicitly generate a streaming response with the new
     96:class:`~django.http.StreamingHttpResponse` class. This class exposes a
     97:class:`~django.http.StreamingHttpResponse.streaming_content` attribute which
     98is an iterator.
     99
     100Since :class:`~django.http.StreamingHttpResponse` does not have a ``content``
     101attribute, middleware that need access to the response content must test for
     102streaming responses and behave accordingly. See :ref:`response-middleware` for
     103more information.
     104
    87105``{% verbatim %}`` template tag
    88106~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    89107
  • docs/topics/http/middleware.txt

    diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt
    index a8347e5..c27e7e8 100644
    a b an earlier middleware method returned an :class:`~django.http.HttpResponse`  
    164164classes are applied in reverse order, from the bottom up. This means classes
    165165defined at the end of :setting:`MIDDLEWARE_CLASSES` will be run first.
    166166
     167.. versionchanged:: 1.5
     168    ``response`` may also be an :class:`~django.http.StreamingHttpResponse`
     169    object.
     170
     171Unlike :class:`~django.http.HttpResponse`,
     172:class:`~django.http.StreamingHttpResponse` does not have a ``content``
     173attribute. As a result, middleware can no longer assume that all responses
     174will have a ``content`` attribute. If they need access to the content, they
     175must test for streaming responses and adjust their behavior accordingly::
     176
     177    if response.streaming:
     178        response.streaming_content = wrap_streaming_content(response.streaming_content)
     179    else:
     180        response.content = wrap_content(response.content)
     181
     182``streaming_content`` should be assumed to be too large to hold in memory.
     183Middleware may wrap it in a new generator, but must not consume it.
    167184
    168185.. _exception-middleware:
    169186
  • tests/regressiontests/cache/tests.py

    diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
    index de27bc9..a6eff89 100644
    a b from django.core.cache import get_cache  
    1919from django.core.cache.backends.base import (CacheKeyWarning,
    2020    InvalidCacheBackendError)
    2121from django.db import router
    22 from django.http import HttpResponse, HttpRequest, QueryDict
     22from django.http import (HttpResponse, HttpRequest, StreamingHttpResponse,
     23    QueryDict)
    2324from django.middleware.cache import (FetchFromCacheMiddleware,
    2425    UpdateCacheMiddleware, CacheMiddleware)
    2526from django.template import Template
    class CacheI18nTest(TestCase):  
    14161417        # reset the language
    14171418        translation.deactivate()
    14181419
     1420    @override_settings(
     1421            CACHE_MIDDLEWARE_KEY_PREFIX="test",
     1422            CACHE_MIDDLEWARE_SECONDS=60,
     1423            USE_ETAGS=True,
     1424    )
     1425    def test_middleware_with_streaming_response(self):
     1426        # cache with non empty request.GET
     1427        request = self._get_request_cache(query_string='foo=baz&other=true')
     1428
     1429        # first access, cache must return None
     1430        get_cache_data = FetchFromCacheMiddleware().process_request(request)
     1431        self.assertEqual(get_cache_data, None)
     1432
     1433        # pass streaming response through UpdateCacheMiddleware.
     1434        content = 'Check for cache with QUERY_STRING and streaming content'
     1435        response = StreamingHttpResponse(content)
     1436        UpdateCacheMiddleware().process_response(request, response)
     1437
     1438        # second access, cache must still return None, because we can't cache
     1439        # streaming response.
     1440        get_cache_data = FetchFromCacheMiddleware().process_request(request)
     1441        self.assertEqual(get_cache_data, None)
     1442
    14191443
    14201444@override_settings(
    14211445        CACHES={
  • new file tests/regressiontests/httpwrappers/abc.txt

    diff --git a/tests/regressiontests/httpwrappers/abc.txt b/tests/regressiontests/httpwrappers/abc.txt
    new file mode 100644
    index 0000000..6bac42b
    - +  
     1random content
  • tests/regressiontests/httpwrappers/tests.py

    diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py
    index 4c6aed1..5aca8ff 100644
    a b  
    22from __future__ import unicode_literals
    33
    44import copy
     5import os
    56import pickle
     7import tempfile
    68
    79from django.core.exceptions import SuspiciousOperation
    810from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
    911                         HttpResponsePermanentRedirect, HttpResponseNotAllowed,
    10                          HttpResponseNotModified,
     12                         HttpResponseNotModified, StreamingHttpResponse,
    1113                         SimpleCookie, BadHeaderError,
    1214                         parse_cookie)
    1315from django.test import TestCase
    class HttpResponseTests(unittest.TestCase):  
    351353            self.assertRaises(SuspiciousOperation,
    352354                              HttpResponsePermanentRedirect, url)
    353355
    354 
    355356class HttpResponseSubclassesTests(TestCase):
    356357    def test_redirect(self):
    357358        response = HttpResponseRedirect('/redirected/')
    class HttpResponseSubclassesTests(TestCase):  
    379380            content_type='text/html')
    380381        self.assertContains(response, 'Only the GET method is allowed', status_code=405)
    381382
     383class StreamingHttpResponseTests(TestCase):
     384    def test_streaming_response(self):
     385        r = StreamingHttpResponse(iter(['hello', 'world']))
     386
     387        # iterating over the response itself yields bytestring chunks.
     388        chunks = list(r)
     389        self.assertEqual(chunks, [b'hello', b'world'])
     390        for chunk in chunks:
     391            self.assertIsInstance(chunk, six.binary_type)
     392
     393        # and the response can only be iterated once.
     394        self.assertEqual(list(r), [])
     395
     396        # even when a sequence that can be iterated many times, like a list,
     397        # is given as content.
     398        r = StreamingHttpResponse(['abc', 'def'])
     399        self.assertEqual(list(r), [b'abc', b'def'])
     400        self.assertEqual(list(r), [])
     401
     402        # streaming responses don't have a `content` attribute.
     403        self.assertFalse(hasattr(r, 'content'))
     404
     405        # and you can't accidentally assign to a `content` attribute.
     406        with self.assertRaises(AttributeError):
     407            r.content = 'xyz'
     408
     409        # but they do have a `streaming_content` attribute.
     410        self.assertTrue(hasattr(r, 'streaming_content'))
     411
     412        # that exists so we can check if a response is streaming, and wrap or
     413        # replace the content iterator.
     414        r.streaming_content = iter(['abc', 'def'])
     415        r.streaming_content = (chunk.upper() for chunk in r.streaming_content)
     416        self.assertEqual(list(r), [b'ABC', b'DEF'])
     417
     418        # coercing a streaming response to bytes doesn't return a complete HTTP
     419        # message like a regular response does. it only gives us the headers.
     420        r = StreamingHttpResponse(iter(['hello', 'world']))
     421        self.assertEqual(
     422            six.binary_type(r), b'Content-Type: text/html; charset=utf-8')
     423
     424        # and this won't consume its content.
     425        self.assertEqual(list(r), [b'hello', b'world'])
     426
     427        # additional content cannot be written to the response.
     428        r = StreamingHttpResponse(iter(['hello', 'world']))
     429        with self.assertRaises(Exception):
     430            r.write('!')
     431
     432        # and we can't tell the current position.
     433        with self.assertRaises(Exception):
     434            r.tell()
     435
     436class FileCloseTests(TestCase):
     437    def test_response(self):
     438        filename = os.path.join(os.path.dirname(__file__), 'abc.txt')
     439
     440        # file isn't closed until we close the response.
     441        file1 = open(filename)
     442        r = HttpResponse(file1)
     443        self.assertFalse(file1.closed)
     444        r.close()
     445        self.assertTrue(file1.closed)
     446
     447        # don't automatically close file when we finish iterating the response.
     448        file1 = open(filename)
     449        r = HttpResponse(file1)
     450        self.assertFalse(file1.closed)
     451        list(r)
     452        self.assertFalse(file1.closed)
     453        r.close()
     454        self.assertTrue(file1.closed)
     455
     456        # when multiple file are assigned as content, make sure they are all
     457        # closed with the response.
     458        file1 = open(filename)
     459        file2 = open(filename)
     460        r = HttpResponse(file1)
     461        r.content = file2
     462        self.assertFalse(file1.closed)
     463        self.assertFalse(file2.closed)
     464        r.close()
     465        self.assertTrue(file1.closed)
     466        self.assertTrue(file2.closed)
     467
     468    def test_streaming_response(self):
     469        filename = os.path.join(os.path.dirname(__file__), 'abc.txt')
     470
     471        # file isn't closed until we close the response.
     472        file1 = open(filename)
     473        r = StreamingHttpResponse(file1)
     474        self.assertFalse(file1.closed)
     475        r.close()
     476        self.assertTrue(file1.closed)
     477
     478        # automatically close file when we finish iterating the response.
     479        file1 = open(filename)
     480        r = StreamingHttpResponse(file1)
     481        self.assertFalse(file1.closed)
     482        list(r)
     483        self.assertTrue(file1.closed)
     484
     485        # when multiple file are assigned as content, make sure they are all
     486        # closed with the response.
     487        file1 = open(filename)
     488        file2 = open(filename)
     489        r = StreamingHttpResponse(file1)
     490        r.streaming_content = file2
     491        self.assertFalse(file1.closed)
     492        self.assertFalse(file2.closed)
     493        r.close()
     494        self.assertTrue(file1.closed)
     495        self.assertTrue(file2.closed)
     496
    382497class CookieTests(unittest.TestCase):
    383498    def test_encode(self):
    384499        """
  • tests/regressiontests/middleware/tests.py

    diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py
    index eb66f2b..de901f4 100644
    a b from io import BytesIO  
    88from django.conf import settings
    99from django.core import mail
    1010from django.http import HttpRequest
    11 from django.http import HttpResponse
     11from django.http import HttpResponse, StreamingHttpResponse
    1212from django.middleware.clickjacking import XFrameOptionsMiddleware
    1313from django.middleware.common import CommonMiddleware
    1414from django.middleware.http import ConditionalGetMiddleware
    class ConditionalGetMiddlewareTest(TestCase):  
    322322        self.assertTrue('Content-Length' in self.resp)
    323323        self.assertEqual(int(self.resp['Content-Length']), content_length)
    324324
     325    def test_content_length_header_not_added(self):
     326        resp = StreamingHttpResponse('content')
     327        self.assertFalse('Content-Length' in resp)
     328        resp = ConditionalGetMiddleware().process_response(self.req, resp)
     329        self.assertFalse('Content-Length' in resp)
     330
    325331    def test_content_length_header_not_changed(self):
    326332        bad_content_length = len(self.resp.content) + 10
    327333        self.resp['Content-Length'] = bad_content_length
    class ConditionalGetMiddlewareTest(TestCase):  
    351357        self.resp = ConditionalGetMiddleware().process_response(self.req, self.resp)
    352358        self.assertEqual(self.resp.status_code, 200)
    353359
     360    @override_settings(USE_ETAGS=True)
     361    def test_etag(self):
     362        req = HttpRequest()
     363        res = HttpResponse('content')
     364        self.assertTrue(
     365            CommonMiddleware().process_response(req, res).has_header('ETag'))
     366
     367    @override_settings(USE_ETAGS=True)
     368    def test_etag_streaming_response(self):
     369        req = HttpRequest()
     370        res = StreamingHttpResponse(['content'])
     371        res['ETag'] = 'tomatoes'
     372        self.assertEqual(
     373            CommonMiddleware().process_response(req, res).get('ETag'),
     374            'tomatoes')
     375
     376    @override_settings(USE_ETAGS=True)
     377    def test_no_etag_streaming_response(self):
     378        req = HttpRequest()
     379        res = StreamingHttpResponse(['content'])
     380        self.assertFalse(
     381            CommonMiddleware().process_response(req, res).has_header('ETag'))
     382
    354383    # Tests for the Last-Modified header
    355384
    356385    def test_if_modified_since_and_no_last_modified(self):
    class GZipMiddlewareTest(TestCase):  
    511540    short_string = b"This string is too short to be worth compressing."
    512541    compressible_string = b'a' * 500
    513542    uncompressible_string = b''.join(six.int2byte(random.randint(0, 255)) for _ in xrange(500))
     543    sequence = [b'a' * 500, b'b' * 200, b'a' * 300]
    514544
    515545    def setUp(self):
    516546        self.req = HttpRequest()
    class GZipMiddlewareTest(TestCase):  
    525555        self.resp.status_code = 200
    526556        self.resp.content = self.compressible_string
    527557        self.resp['Content-Type'] = 'text/html; charset=UTF-8'
     558        self.stream_resp = StreamingHttpResponse(self.sequence)
     559        self.stream_resp['Content-Type'] = 'text/html; charset=UTF-8'
    528560
    529561    @staticmethod
    530562    def decompress(gzipped_string):
    class GZipMiddlewareTest(TestCase):  
    539571        self.assertEqual(r.get('Content-Encoding'), 'gzip')
    540572        self.assertEqual(r.get('Content-Length'), str(len(r.content)))
    541573
     574    def test_compress_streaming_response(self):
     575        """
     576        Tests that compression is performed on responses with streaming content.
     577        """
     578        r = GZipMiddleware().process_response(self.req, self.stream_resp)
     579        self.assertEqual(self.decompress(b''.join(r)), b''.join(self.sequence))
     580        self.assertEqual(r.get('Content-Encoding'), 'gzip')
     581        self.assertFalse(r.has_header('Content-Length'))
     582
    542583    def test_compress_non_200_response(self):
    543584        """
    544585        Tests that compression is performed on responses with a status other than 200.
Back to Top