Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #6527 -- Provided repeatable content access

in HttpResponses instantiated with iterators.
  • Loading branch information...
commit 495a8b8107dbd4fb511954bcd2322d125addd94e 1 parent 83041ca
Aymeric Augustin authored October 23, 2012
30  django/http/response.py
@@ -246,8 +246,18 @@ def serialize(self):
246 246
     else:
247 247
         __str__ = serialize
248 248
 
  249
+    def _consume_content(self):
  250
+        # If the response was instantiated with an iterator, when its content
  251
+        # is accessed, the iterator is going be exhausted and the content
  252
+        # loaded in memory. At this point, it's better to abandon the original
  253
+        # iterator and save the content for later reuse. This is a temporary
  254
+        # solution. See the comment in __iter__ below for the long term plan.
  255
+        if self._base_content_is_iter:
  256
+            self.content = b''.join(self.make_bytes(e) for e in self._container)
  257
+
249 258
     @property
250 259
     def content(self):
  260
+        self._consume_content()
251 261
         return b''.join(self.make_bytes(e) for e in self._container)
252 262
 
253 263
     @content.setter
@@ -262,6 +272,17 @@ def content(self, value):
262 272
             self._base_content_is_iter = False
263 273
 
264 274
     def __iter__(self):
  275
+        # Raise a deprecation warning only if the content wasn't consumed yet,
  276
+        # because the response may be intended to be streamed.
  277
+        # Once the deprecation completes, iterators should be consumed upon
  278
+        # assignment rather than upon access. The _consume_content method
  279
+        # should be removed. See #6527.
  280
+        if self._base_content_is_iter:
  281
+            warnings.warn(
  282
+                'Creating streaming responses with `HttpResponse` is '
  283
+                'deprecated. Use `StreamingHttpResponse` instead '
  284
+                'if you need the streaming behavior.',
  285
+                PendingDeprecationWarning, stacklevel=2)
265 286
         self._iterator = iter(self._container)
266 287
         return self
267 288
 
@@ -277,14 +298,12 @@ def __next__(self):
277 298
     next = __next__             # Python 2 compatibility
278 299
 
279 300
     def write(self, content):
280  
-        if self._base_content_is_iter:
281  
-            raise Exception("This %s instance is not writable" % self.__class__.__name__)
  301
+        self._consume_content()
282 302
         self._container.append(content)
283 303
 
284 304
     def tell(self):
285  
-        if self._base_content_is_iter:
286  
-            raise Exception("This %s instance cannot tell its position" % self.__class__.__name__)
287  
-        return sum([len(chunk) for chunk in self])
  305
+        self._consume_content()
  306
+        return sum(len(chunk) for chunk in self)
288 307
 
289 308
 
290 309
 class StreamingHttpResponse(HttpResponseBase):
@@ -389,6 +408,7 @@ def content(self, value):
389 408
         if value:
390 409
             raise AttributeError("You cannot set content to a 304 (Not Modified) response")
391 410
         self._container = []
  411
+        self._base_content_is_iter = False
392 412
 
393 413
 
394 414
 class HttpResponseBadRequest(HttpResponse):
4  docs/internals/deprecation.txt
@@ -286,6 +286,10 @@ these changes.
286 286
 * The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__``
287 287
   will be removed (``content_type`` should be used instead).
288 288
 
  289
+* When :class:`~django.http.HttpResponse` is instantiated with an iterator,
  290
+  or when :attr:`~django.http.HttpResponse.content` is set to an iterator,
  291
+  that iterator will be immediately consumed.
  292
+
289 293
 * The ``AUTH_PROFILE_MODULE`` setting, and the ``get_profile()`` method on
290 294
   the User model, will be removed.
291 295
 
23  docs/ref/request-response.txt
@@ -569,18 +569,25 @@ Passing iterators
569 569
 Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you
570 570
 use this technique, the iterator should return strings.
571 571
 
  572
+Passing an iterator as content to :class:`HttpResponse` creates a
  573
+streaming response if (and only if) no middleware accesses the
  574
+:attr:`HttpResponse.content` attribute before the response is returned.
  575
+
572 576
 .. versionchanged:: 1.5
573 577
 
574  
-    Passing an iterator as content to :class:`HttpResponse` creates a
575  
-    streaming response if (and only if) no middleware accesses the
576  
-    :attr:`HttpResponse.content` attribute before the response is returned.
  578
+This technique is fragile and was deprecated in Django 1.5. If you need the
  579
+response to be streamed from the iterator to the client, you should use the
  580
+:class:`StreamingHttpResponse` class instead.
  581
+
  582
+As of Django 1.7, when :class:`HttpResponse` is instantiated with an
  583
+iterator, it will consume it immediately, store the response content as a
  584
+string, and discard the iterator.
577 585
 
578  
-    If you want to guarantee that your response will stream to the client, you
579  
-    should use the new :class:`StreamingHttpResponse` class instead.
  586
+.. versionchanged:: 1.5
580 587
 
581  
-If an :class:`HttpResponse` instance has been initialized with an iterator as
582  
-its content, you can't use it as a file-like object. Doing so will raise an
583  
-exception.
  588
+You can now use :class:`HttpResponse` as a file-like object even if it was
  589
+instantiated with an iterator. Django will consume and save the content of
  590
+the iterator on first access.
584 591
 
585 592
 Setting headers
586 593
 ~~~~~~~~~~~~~~~
54  docs/releases/1.5.txt
@@ -84,6 +84,8 @@ For one-to-one relationships, both sides can be cached. For many-to-one
84 84
 relationships, only the single side of the relationship can be cached. This
85 85
 is particularly helpful in combination with ``prefetch_related``.
86 86
 
  87
+.. _explicit-streaming-responses:
  88
+
87 89
 Explicit support for streaming responses
88 90
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
89 91
 
@@ -98,7 +100,7 @@ You can now explicitly generate a streaming response with the new
98 100
 is an iterator.
99 101
 
100 102
 Since :class:`~django.http.StreamingHttpResponse` does not have a ``content``
101  
-attribute, middleware that need access to the response content must test for
  103
+attribute, middleware that needs access to the response content must test for
102 104
 streaming responses and behave accordingly. See :ref:`response-middleware` for
103 105
 more information.
104 106
 
@@ -483,6 +485,30 @@ Features deprecated in 1.5
483 485
 
484 486
 .. _simplejson-deprecation:
485 487
 
  488
+:setting:`AUTH_PROFILE_MODULE`
  489
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  490
+
  491
+With the introduction of :ref:`custom User models <auth-custom-user>`, there is
  492
+no longer any need for a built-in mechanism to store user profile data.
  493
+
  494
+You can still define user profiles models that have a one-to-one relation with
  495
+the User model - in fact, for many applications needing to associate data with
  496
+a User account, this will be an appropriate design pattern to follow. However,
  497
+the :setting:`AUTH_PROFILE_MODULE` setting, and the
  498
+:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing
  499
+the user profile model, should not be used any longer.
  500
+
  501
+Streaming behavior of :class:`HttpResponse`
  502
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  503
+
  504
+Django 1.5 deprecates the ability to stream a response by passing an iterator
  505
+to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to
  506
+:class:`~django.http.StreamingHttpResponse`. See :ref:`explicit-streaming-
  507
+responses` above.
  508
+
  509
+In Django 1.7 and above, the iterator will be consumed immediately by
  510
+:class:`~django.http.HttpResponse`.
  511
+
486 512
 ``django.utils.simplejson``
487 513
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
488 514
 
@@ -497,12 +523,6 @@ incompatibilities between versions of :mod:`simplejson` -- see the
497 523
 If you rely on features added to :mod:`simplejson` after it became Python's
498 524
 :mod:`json`, you should import :mod:`simplejson` explicitly.
499 525
 
500  
-``itercompat.product``
501  
-~~~~~~~~~~~~~~~~~~~~~~
502  
-
503  
-The :func:`~django.utils.itercompat.product` function has been deprecated. Use
504  
-the built-in :func:`itertools.product` instead.
505  
-
506 526
 ``django.utils.encoding.StrAndUnicode``
507 527
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
508 528
 
@@ -510,6 +530,13 @@ The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated.
510 530
 Define a ``__str__`` method and apply the
511 531
 :func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead.
512 532
 
  533
+``django.utils.itercompat.product``
  534
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  535
+
  536
+The :func:`~django.utils.itercompat.product` function has been deprecated. Use
  537
+the built-in :func:`itertools.product` instead.
  538
+
  539
+
513 540
 ``django.utils.markup``
514 541
 ~~~~~~~~~~~~~~~~~~~~~~~
515 542
 
@@ -517,16 +544,3 @@ The markup contrib module has been deprecated and will follow an accelerated
517 544
 deprecation schedule. Direct use of python markup libraries or 3rd party tag
518 545
 libraries is preferred to Django maintaining this functionality in the
519 546
 framework.
520  
-
521  
-:setting:`AUTH_PROFILE_MODULE`
522  
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
523  
-
524  
-With the introduction of :ref:`custom User models <auth-custom-user>`, there is
525  
-no longer any need for a built-in mechanism to store user profile data.
526  
-
527  
-You can still define user profiles models that have a one-to-one relation with
528  
-the User model - in fact, for many applications needing to associate data with
529  
-a User account, this will be an appropriate design pattern to follow. However,
530  
-the :setting:`AUTH_PROFILE_MODULE` setting, and the
531  
-:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing
532  
-the user profile model, should not be used any longer.
31  tests/regressiontests/httpwrappers/tests.py
@@ -4,6 +4,7 @@
4 4
 import copy
5 5
 import os
6 6
 import pickle
  7
+import warnings
7 8
 
8 9
 from django.core.exceptions import SuspiciousOperation
9 10
 from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
@@ -313,11 +314,17 @@ def test_iter_content(self):
313 314
         r.content = [1, 2, 3]
314 315
         self.assertEqual(r.content, b'123')
315 316
 
316  
-        #test retrieval explicitly using iter and odd inputs
  317
+        #test retrieval explicitly using iter (deprecated) and odd inputs
317 318
         r = HttpResponse()
318 319
         r.content = ['1', '2', 3, '\u079e']
319  
-        my_iter = r.__iter__()
320  
-        result = list(my_iter)
  320
+        with warnings.catch_warnings(record=True) as w:
  321
+            warnings.simplefilter("always", PendingDeprecationWarning)
  322
+            my_iter = iter(r)
  323
+            self.assertEqual(w[0].category, PendingDeprecationWarning)
  324
+        with warnings.catch_warnings(record=True) as w:
  325
+            warnings.simplefilter("always", PendingDeprecationWarning)
  326
+            result = list(my_iter)
  327
+            self.assertEqual(w[0].category, PendingDeprecationWarning)
321 328
         #'\xde\x9e' == unichr(1950).encode('utf-8')
322 329
         self.assertEqual(result, [b'1', b'2', b'3', b'\xde\x9e'])
323 330
         self.assertEqual(r.content, b'123\xde\x9e')
@@ -330,6 +337,16 @@ def test_iter_content(self):
330 337
         self.assertRaises(UnicodeEncodeError,
331 338
                           getattr, r, 'content')
332 339
 
  340
+        # content can safely be accessed multiple times.
  341
+        r = HttpResponse(iter(['hello', 'world']))
  342
+        self.assertEqual(r.content, r.content)
  343
+        self.assertEqual(r.content, b'helloworld')
  344
+
  345
+        # additional content can be written to the response.
  346
+        r.write('!')
  347
+        self.assertEqual(r.content, b'helloworld!')
  348
+
  349
+
333 350
     def test_file_interface(self):
334 351
         r = HttpResponse()
335 352
         r.write(b"hello")
@@ -338,7 +355,9 @@ def test_file_interface(self):
338 355
         self.assertEqual(r.tell(), 17)
339 356
 
340 357
         r = HttpResponse(['abc'])
341  
-        self.assertRaises(Exception, r.write, 'def')
  358
+        r.write('def')
  359
+        self.assertEqual(r.tell(), 6)
  360
+        self.assertEqual(r.content, b'abcdef')
342 361
 
343 362
     def test_unsafe_redirect(self):
344 363
         bad_urls = [
@@ -447,7 +466,9 @@ def test_response(self):
447 466
         file1 = open(filename)
448 467
         r = HttpResponse(file1)
449 468
         self.assertFalse(file1.closed)
450  
-        list(r)
  469
+        with warnings.catch_warnings():
  470
+            warnings.simplefilter("ignore", PendingDeprecationWarning)
  471
+            list(r)
451 472
         self.assertFalse(file1.closed)
452 473
         r.close()
453 474
         self.assertTrue(file1.closed)

0 notes on commit 495a8b8

Please sign in to comment.
Something went wrong with that request. Please try again.