Browse Source

Fixed #31789 -- Added a new headers interface to HttpResponse.

Tom Carrick 4 years ago
parent
commit
bcc2befd0e
47 changed files with 385 additions and 256 deletions
  1. 1 1
      django/contrib/admin/tests.py
  2. 1 1
      django/contrib/admindocs/middleware.py
  3. 2 2
      django/contrib/sitemaps/views.py
  4. 1 1
      django/contrib/syndication/views.py
  5. 78 50
      django/http/response.py
  6. 4 2
      django/middleware/clickjacking.py
  7. 1 1
      django/middleware/common.py
  8. 4 4
      django/middleware/gzip.py
  9. 1 1
      django/middleware/locale.py
  10. 4 4
      django/middleware/security.py
  11. 11 11
      django/utils/cache.py
  12. 2 2
      django/views/decorators/http.py
  13. 2 2
      django/views/generic/base.py
  14. 2 2
      django/views/static.py
  15. 3 3
      docs/howto/outputting-csv.txt
  16. 27 4
      docs/ref/request-response.txt
  17. 4 1
      docs/releases/3.2.txt
  18. 4 4
      docs/topics/cache.txt
  19. 1 1
      docs/topics/class-based-views/index.txt
  20. 3 3
      docs/topics/testing/tools.txt
  21. 3 3
      tests/admin_docs/test_middleware.py
  22. 1 1
      tests/admin_views/tests.py
  23. 1 1
      tests/auth_tests/test_views.py
  24. 9 9
      tests/cache/tests.py
  25. 4 4
      tests/conditional_processing/tests.py
  26. 2 2
      tests/contenttypes_tests/test_views.py
  27. 3 3
      tests/decorators/tests.py
  28. 5 5
      tests/generic_views/test_base.py
  29. 59 22
      tests/httpwrappers/tests.py
  30. 12 12
      tests/i18n/patterns/tests.py
  31. 46 19
      tests/middleware/test_security.py
  32. 24 24
      tests/middleware/tests.py
  33. 23 14
      tests/responses/test_fileresponse.py
  34. 5 5
      tests/responses/tests.py
  35. 4 4
      tests/sessions_tests/tests.py
  36. 2 2
      tests/shortcuts/tests.py
  37. 1 1
      tests/sitemaps_tests/test_generic.py
  38. 7 7
      tests/sitemaps_tests/test_http.py
  39. 2 2
      tests/syndication_tests/tests.py
  40. 6 6
      tests/template_tests/test_response.py
  41. 1 1
      tests/test_client/tests.py
  42. 1 1
      tests/test_client/views.py
  43. 1 1
      tests/test_client_regress/tests.py
  44. 1 1
      tests/view_tests/tests/test_debug.py
  45. 1 1
      tests/view_tests/tests/test_i18n.py
  46. 1 1
      tests/view_tests/tests/test_json.py
  47. 4 4
      tests/view_tests/tests/test_static.py

+ 1 - 1
django/contrib/admin/tests.py

@@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
 class CSPMiddleware(MiddlewareMixin):
     """The admin's JavaScript should be compatible with CSP."""
     def process_response(self, request, response):
-        response['Content-Security-Policy'] = "default-src 'self'"
+        response.headers['Content-Security-Policy'] = "default-src 'self'"
         return response
 
 

+ 1 - 1
django/contrib/admindocs/middleware.py

@@ -24,5 +24,5 @@ class XViewMiddleware(MiddlewareMixin):
         if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or
                                          (request.user.is_active and request.user.is_staff)):
             response = HttpResponse()
-            response['X-View'] = get_view_name(view_func)
+            response.headers['X-View'] = get_view_name(view_func)
             return response

+ 2 - 2
django/contrib/sitemaps/views.py

@@ -14,7 +14,7 @@ def x_robots_tag(func):
     @wraps(func)
     def inner(request, *args, **kwargs):
         response = func(request, *args, **kwargs)
-        response['X-Robots-Tag'] = 'noindex, noodp, noarchive'
+        response.headers['X-Robots-Tag'] = 'noindex, noodp, noarchive'
         return response
     return inner
 
@@ -88,5 +88,5 @@ def sitemap(request, sitemaps, section=None,
     if all_sites_lastmod and lastmod is not None:
         # if lastmod is defined for all sites, set header so as
         # ConditionalGetMiddleware is able to send 304 NOT MODIFIED
-        response['Last-Modified'] = http_date(timegm(lastmod))
+        response.headers['Last-Modified'] = http_date(timegm(lastmod))
     return response

+ 1 - 1
django/contrib/syndication/views.py

@@ -42,7 +42,7 @@ class Feed:
         if hasattr(self, 'item_pubdate') or hasattr(self, 'item_updateddate'):
             # if item_pubdate or item_updateddate is defined for the feed, set
             # header so as ConditionalGetMiddleware is able to send 304 NOT MODIFIED
-            response['Last-Modified'] = http_date(
+            response.headers['Last-Modified'] = http_date(
                 timegm(feedgen.latest_post_date().utctimetuple()))
         feedgen.write(response, 'utf-8')
         return response

+ 78 - 50
django/http/response.py

@@ -5,6 +5,7 @@ import os
 import re
 import sys
 import time
+from collections.abc import Mapping
 from email.header import Header
 from http.client import responses
 from urllib.parse import quote, urlparse
@@ -15,6 +16,7 @@ from django.core.exceptions import DisallowedRedirect
 from django.core.serializers.json import DjangoJSONEncoder
 from django.http.cookie import SimpleCookie
 from django.utils import timezone
+from django.utils.datastructures import CaseInsensitiveMapping
 from django.utils.encoding import iri_to_uri
 from django.utils.http import http_date
 from django.utils.regex_helper import _lazy_re_compile
@@ -22,6 +24,65 @@ from django.utils.regex_helper import _lazy_re_compile
 _charset_from_content_type_re = _lazy_re_compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I)
 
 
+class ResponseHeaders(CaseInsensitiveMapping):
+    def __init__(self, data):
+        """
+        Populate the initial data using __setitem__ to ensure values are
+        correctly encoded.
+        """
+        if not isinstance(data, Mapping):
+            data = {
+                k: v
+                for k, v in CaseInsensitiveMapping._destruct_iterable_mapping_values(data)
+            }
+        self._store = {}
+        for header, value in data.items():
+            self[header] = value
+
+    def _convert_to_charset(self, value, charset, mime_encode=False):
+        """
+        Convert headers key/value to ascii/latin-1 native strings.
+        `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
+        `value` can't be represented in the given charset, apply MIME-encoding.
+        """
+        if not isinstance(value, (bytes, str)):
+            value = str(value)
+        if (
+            (isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or
+            (isinstance(value, str) and ('\n' in value or '\r' in value))
+        ):
+            raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
+        try:
+            if isinstance(value, str):
+                # Ensure string is valid in given charset
+                value.encode(charset)
+            else:
+                # Convert bytestring using given charset
+                value = value.decode(charset)
+        except UnicodeError as e:
+            if mime_encode:
+                value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()
+            else:
+                e.reason += ', HTTP response headers must be in %s format' % charset
+                raise
+        return value
+
+    def __delitem__(self, key):
+        self.pop(key)
+
+    def __setitem__(self, key, value):
+        key = self._convert_to_charset(key, 'ascii')
+        value = self._convert_to_charset(value, 'latin-1', mime_encode=True)
+        self._store[key.lower()] = (key, value)
+
+    def pop(self, key, default=None):
+        return self._store.pop(key.lower(), default)
+
+    def setdefault(self, key, value):
+        if key not in self:
+            self[key] = value
+
+
 class BadHeaderError(ValueError):
     pass
 
@@ -37,10 +98,7 @@ class HttpResponseBase:
     status_code = 200
 
     def __init__(self, content_type=None, status=None, reason=None, charset=None):
-        # _headers is a mapping of the lowercase name to the original case of
-        # the header (required for working with legacy systems) and the header
-        # value. Both the name of the header and its value are ASCII strings.
-        self._headers = {}
+        self.headers = ResponseHeaders({})
         self._resource_closers = []
         # This parameter is set by the handler. It's necessary to preserve the
         # historical behavior of request_finished.
@@ -95,7 +153,7 @@ class HttpResponseBase:
 
         headers = [
             (to_bytes(key, 'ascii') + b': ' + to_bytes(value, 'latin-1'))
-            for key, value in self._headers.values()
+            for key, value in self.headers.items()
         ]
         return b'\r\n'.join(headers)
 
@@ -103,57 +161,28 @@ class HttpResponseBase:
 
     @property
     def _content_type_for_repr(self):
-        return ', "%s"' % self['Content-Type'] if 'Content-Type' in self else ''
-
-    def _convert_to_charset(self, value, charset, mime_encode=False):
-        """
-        Convert headers key/value to ascii/latin-1 native strings.
-
-        `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
-        `value` can't be represented in the given charset, apply MIME-encoding.
-        """
-        if not isinstance(value, (bytes, str)):
-            value = str(value)
-        if ((isinstance(value, bytes) and (b'\n' in value or b'\r' in value)) or
-                isinstance(value, str) and ('\n' in value or '\r' in value)):
-            raise BadHeaderError("Header values can't contain newlines (got %r)" % value)
-        try:
-            if isinstance(value, str):
-                # Ensure string is valid in given charset
-                value.encode(charset)
-            else:
-                # Convert bytestring using given charset
-                value = value.decode(charset)
-        except UnicodeError as e:
-            if mime_encode:
-                value = Header(value, 'utf-8', maxlinelen=sys.maxsize).encode()
-            else:
-                e.reason += ', HTTP response headers must be in %s format' % charset
-                raise
-        return value
+        return ', "%s"' % self.headers['Content-Type'] if 'Content-Type' in self.headers else ''
 
     def __setitem__(self, header, value):
-        header = self._convert_to_charset(header, 'ascii')
-        value = self._convert_to_charset(value, 'latin-1', mime_encode=True)
-        self._headers[header.lower()] = (header, value)
+        self.headers[header] = value
 
     def __delitem__(self, header):
-        self._headers.pop(header.lower(), False)
+        del self.headers[header]
 
     def __getitem__(self, header):
-        return self._headers[header.lower()][1]
+        return self.headers[header]
 
     def has_header(self, header):
         """Case-insensitive check for a header."""
-        return header.lower() in self._headers
+        return header in self.headers
 
     __contains__ = has_header
 
     def items(self):
-        return self._headers.values()
+        return self.headers.items()
 
     def get(self, header, alternate=None):
-        return self._headers.get(header.lower(), (None, alternate))[1]
+        return self.headers.get(header, alternate)
 
     def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
                    domain=None, secure=False, httponly=False, samesite=None):
@@ -203,8 +232,7 @@ class HttpResponseBase:
 
     def setdefault(self, key, value):
         """Set a header unless it has already been set."""
-        if key not in self:
-            self[key] = value
+        self.headers.setdefault(key, value)
 
     def set_signed_cookie(self, key, value, salt='', **kwargs):
         value = signing.get_cookie_signer(salt=key + salt).sign(value)
@@ -430,19 +458,19 @@ class FileResponse(StreamingHttpResponse):
         filename = getattr(filelike, 'name', None)
         filename = filename if (isinstance(filename, str) and filename) else self.filename
         if os.path.isabs(filename):
-            self['Content-Length'] = os.path.getsize(filelike.name)
+            self.headers['Content-Length'] = os.path.getsize(filelike.name)
         elif hasattr(filelike, 'getbuffer'):
-            self['Content-Length'] = filelike.getbuffer().nbytes
+            self.headers['Content-Length'] = filelike.getbuffer().nbytes
 
-        if self.get('Content-Type', '').startswith('text/html'):
+        if self.headers.get('Content-Type', '').startswith('text/html'):
             if filename:
                 content_type, encoding = mimetypes.guess_type(filename)
                 # Encoding isn't set to prevent browsers from automatically
                 # uncompressing files.
                 content_type = encoding_map.get(encoding, content_type)
-                self['Content-Type'] = content_type or 'application/octet-stream'
+                self.headers['Content-Type'] = content_type or 'application/octet-stream'
             else:
-                self['Content-Type'] = 'application/octet-stream'
+                self.headers['Content-Type'] = 'application/octet-stream'
 
         filename = self.filename or os.path.basename(filename)
         if filename:
@@ -452,9 +480,9 @@ class FileResponse(StreamingHttpResponse):
                 file_expr = 'filename="{}"'.format(filename)
             except UnicodeEncodeError:
                 file_expr = "filename*=utf-8''{}".format(quote(filename))
-            self['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
+            self.headers['Content-Disposition'] = '{}; {}'.format(disposition, file_expr)
         elif self.as_attachment:
-            self['Content-Disposition'] = 'attachment'
+            self.headers['Content-Disposition'] = 'attachment'
 
 
 class HttpResponseRedirectBase(HttpResponse):

+ 4 - 2
django/middleware/clickjacking.py

@@ -30,8 +30,10 @@ class XFrameOptionsMiddleware(MiddlewareMixin):
         if getattr(response, 'xframe_options_exempt', False):
             return response
 
-        response['X-Frame-Options'] = self.get_xframe_options_value(request,
-                                                                    response)
+        response.headers['X-Frame-Options'] = self.get_xframe_options_value(
+            request,
+            response,
+        )
         return response
 
     def get_xframe_options_value(self, request, response):

+ 1 - 1
django/middleware/common.py

@@ -110,7 +110,7 @@ class CommonMiddleware(MiddlewareMixin):
         # Add the Content-Length header to non-streaming responses if not
         # already set.
         if not response.streaming and not response.has_header('Content-Length'):
-            response['Content-Length'] = str(len(response.content))
+            response.headers['Content-Length'] = str(len(response.content))
 
         return response
 

+ 4 - 4
django/middleware/gzip.py

@@ -31,21 +31,21 @@ class GZipMiddleware(MiddlewareMixin):
             # Delete the `Content-Length` header for streaming content, because
             # we won't know the compressed size until we stream it.
             response.streaming_content = compress_sequence(response.streaming_content)
-            del response['Content-Length']
+            del response.headers['Content-Length']
         else:
             # Return the compressed content only if it's actually shorter.
             compressed_content = compress_string(response.content)
             if len(compressed_content) >= len(response.content):
                 return response
             response.content = compressed_content
-            response['Content-Length'] = str(len(response.content))
+            response.headers['Content-Length'] = str(len(response.content))
 
         # If there is a strong ETag, make it weak to fulfill the requirements
         # of RFC 7232 section-2.1 while also allowing conditional request
         # matches on ETags.
         etag = response.get('ETag')
         if etag and etag.startswith('"'):
-            response['ETag'] = 'W/' + etag
-        response['Content-Encoding'] = 'gzip'
+            response.headers['ETag'] = 'W/' + etag
+        response.headers['Content-Encoding'] = 'gzip'
 
         return response

+ 1 - 1
django/middleware/locale.py

@@ -57,5 +57,5 @@ class LocaleMiddleware(MiddlewareMixin):
 
         if not (i18n_patterns_used and language_from_path):
             patch_vary_headers(response, ('Accept-Language',))
-        response.setdefault('Content-Language', language)
+        response.headers.setdefault('Content-Language', language)
         return response

+ 4 - 4
django/middleware/security.py

@@ -38,18 +38,18 @@ class SecurityMiddleware(MiddlewareMixin):
                 sts_header = sts_header + "; includeSubDomains"
             if self.sts_preload:
                 sts_header = sts_header + "; preload"
-            response['Strict-Transport-Security'] = sts_header
+            response.headers['Strict-Transport-Security'] = sts_header
 
         if self.content_type_nosniff:
-            response.setdefault('X-Content-Type-Options', 'nosniff')
+            response.headers.setdefault('X-Content-Type-Options', 'nosniff')
 
         if self.xss_filter:
-            response.setdefault('X-XSS-Protection', '1; mode=block')
+            response.headers.setdefault('X-XSS-Protection', '1; mode=block')
 
         if self.referrer_policy:
             # Support a comma-separated string or iterable of values to allow
             # fallback.
-            response.setdefault('Referrer-Policy', ','.join(
+            response.headers.setdefault('Referrer-Policy', ','.join(
                 [v.strip() for v in self.referrer_policy.split(',')]
                 if isinstance(self.referrer_policy, str) else self.referrer_policy
             ))

+ 11 - 11
django/utils/cache.py

@@ -62,7 +62,7 @@ def patch_cache_control(response, **kwargs):
 
     cc = defaultdict(set)
     if response.get('Cache-Control'):
-        for field in cc_delim_re.split(response['Cache-Control']):
+        for field in cc_delim_re.split(response.headers['Cache-Control']):
             directive, value = dictitem(field)
             if directive == 'no-cache':
                 # no-cache supports multiple field names.
@@ -100,7 +100,7 @@ def patch_cache_control(response, **kwargs):
         else:
             directives.append(dictvalue(directive, values))
     cc = ', '.join(directives)
-    response['Cache-Control'] = cc
+    response.headers['Cache-Control'] = cc
 
 
 def get_max_age(response):
@@ -110,7 +110,7 @@ def get_max_age(response):
     """
     if not response.has_header('Cache-Control'):
         return
-    cc = dict(_to_tuple(el) for el in cc_delim_re.split(response['Cache-Control']))
+    cc = dict(_to_tuple(el) for el in cc_delim_re.split(response.headers['Cache-Control']))
     try:
         return int(cc['max-age'])
     except (ValueError, TypeError, KeyError):
@@ -119,7 +119,7 @@ def get_max_age(response):
 
 def set_response_etag(response):
     if not response.streaming and response.content:
-        response['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest())
+        response.headers['ETag'] = quote_etag(hashlib.md5(response.content).hexdigest())
     return response
 
 
@@ -140,7 +140,7 @@ def _not_modified(request, response=None):
         # Last-Modified.
         for header in ('Cache-Control', 'Content-Location', 'Date', 'ETag', 'Expires', 'Last-Modified', 'Vary'):
             if header in response:
-                new_response[header] = response[header]
+                new_response.headers[header] = response.headers[header]
 
         # Preserve cookies as per the cookie specification: "If a proxy server
         # receives a response which contains a Set-cookie header, it should
@@ -261,7 +261,7 @@ def patch_response_headers(response, cache_timeout=None):
     if cache_timeout < 0:
         cache_timeout = 0  # Can't have max-age negative
     if not response.has_header('Expires'):
-        response['Expires'] = http_date(time.time() + cache_timeout)
+        response.headers['Expires'] = http_date(time.time() + cache_timeout)
     patch_cache_control(response, max_age=cache_timeout)
 
 
@@ -284,7 +284,7 @@ def patch_vary_headers(response, newheaders):
     # implementations may rely on the order of the Vary contents in, say,
     # computing an MD5 hash.
     if response.has_header('Vary'):
-        vary_headers = cc_delim_re.split(response['Vary'])
+        vary_headers = cc_delim_re.split(response.headers['Vary'])
     else:
         vary_headers = []
     # Use .lower() here so we treat headers as case-insensitive.
@@ -293,9 +293,9 @@ def patch_vary_headers(response, newheaders):
                           if newheader.lower() not in existing_headers]
     vary_headers += additional_headers
     if '*' in vary_headers:
-        response['Vary'] = '*'
+        response.headers['Vary'] = '*'
     else:
-        response['Vary'] = ', '.join(vary_headers)
+        response.headers['Vary'] = ', '.join(vary_headers)
 
 
 def has_vary_header(response, header_query):
@@ -304,7 +304,7 @@ def has_vary_header(response, header_query):
     """
     if not response.has_header('Vary'):
         return False
-    vary_headers = cc_delim_re.split(response['Vary'])
+    vary_headers = cc_delim_re.split(response.headers['Vary'])
     existing_headers = {header.lower() for header in vary_headers}
     return header_query.lower() in existing_headers
 
@@ -391,7 +391,7 @@ def learn_cache_key(request, response, cache_timeout=None, key_prefix=None, cach
         # in that case and would result in storing the same content under
         # multiple keys in the cache. See #18191 for details.
         headerlist = []
-        for header in cc_delim_re.split(response['Vary']):
+        for header in cc_delim_re.split(response.headers['Vary']):
             header = header.upper().replace('-', '_')
             if header != 'ACCEPT_LANGUAGE' or not is_accept_language_redundant:
                 headerlist.append('HTTP_' + header)

+ 2 - 2
django/views/decorators/http.py

@@ -102,9 +102,9 @@ def condition(etag_func=None, last_modified_func=None):
             # and if the request method is safe.
             if request.method in ('GET', 'HEAD'):
                 if res_last_modified and not response.has_header('Last-Modified'):
-                    response['Last-Modified'] = http_date(res_last_modified)
+                    response.headers['Last-Modified'] = http_date(res_last_modified)
                 if res_etag:
-                    response.setdefault('ETag', res_etag)
+                    response.headers.setdefault('ETag', res_etag)
 
             return response
 

+ 2 - 2
django/views/generic/base.py

@@ -107,8 +107,8 @@ class View:
     def options(self, request, *args, **kwargs):
         """Handle responding to requests for the OPTIONS HTTP verb."""
         response = HttpResponse()
-        response['Allow'] = ', '.join(self._allowed_methods())
-        response['Content-Length'] = '0'
+        response.headers['Allow'] = ', '.join(self._allowed_methods())
+        response.headers['Content-Length'] = '0'
         return response
 
     def _allowed_methods(self):

+ 2 - 2
django/views/static.py

@@ -48,9 +48,9 @@ def serve(request, path, document_root=None, show_indexes=False):
     content_type, encoding = mimetypes.guess_type(str(fullpath))
     content_type = content_type or 'application/octet-stream'
     response = FileResponse(fullpath.open('rb'), content_type=content_type)
-    response["Last-Modified"] = http_date(statobj.st_mtime)
+    response.headers["Last-Modified"] = http_date(statobj.st_mtime)
     if encoding:
-        response["Content-Encoding"] = encoding
+        response.headers["Content-Encoding"] = encoding
     return response
 
 

+ 3 - 3
docs/howto/outputting-csv.txt

@@ -21,7 +21,7 @@ Here's an example::
     def some_view(request):
         # Create the HttpResponse object with the appropriate CSV header.
         response = HttpResponse(content_type='text/csv')
-        response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
+        response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
 
         writer = csv.writer(response)
         writer.writerow(['First row', 'Foo', 'Bar', 'Baz'])
@@ -88,7 +88,7 @@ the assembly and transmission of a large CSV file::
         writer = csv.writer(pseudo_buffer)
         response = StreamingHttpResponse((writer.writerow(row) for row in rows),
                                          content_type="text/csv")
-        response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
+        response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
         return response
 
 Using the template system
@@ -109,7 +109,7 @@ Here's an example, which generates the same CSV file as above::
     def some_view(request):
         # Create the HttpResponse object with the appropriate CSV header.
         response = HttpResponse(content_type='text/csv')
-        response['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
+        response.headers['Content-Disposition'] = 'attachment; filename="somefilename.csv"'
 
         # The data is hard-coded here, but you could load it from a database or
         # some other source.

+ 27 - 4
docs/ref/request-response.txt

@@ -700,17 +700,29 @@ generators are immediately closed.
 If you need the response to be streamed from the iterator to the client, you
 must use the :class:`StreamingHttpResponse` class instead.
 
+.. _setting-header-fields:
+
 Setting header fields
 ~~~~~~~~~~~~~~~~~~~~~
 
-To set or remove a header field in your response, treat it like a dictionary::
+To set or remove a header field in your response, use
+:attr:`HttpResponse.headers`::
+
+    >>> response = HttpResponse()
+    >>> response.headers['Age'] = 120
+    >>> del response.headers['Age']
+
+You can also manipulate headers by treating your response like a dictionary::
 
     >>> response = HttpResponse()
     >>> response['Age'] = 120
     >>> del response['Age']
 
-Note that unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header
-field doesn't exist.
+This proxies to ``HttpResponse.headers``, and is the original interface offered
+by ``HttpResponse``.
+
+When using this interface, unlike a dictionary, ``del`` doesn't raise
+``KeyError`` if the header field doesn't exist.
 
 For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended
 to use the :func:`~django.utils.cache.patch_cache_control` and
@@ -722,6 +734,10 @@ middleware, are not removed.
 HTTP header fields cannot contain newlines. An attempt to set a header field
 containing a newline character (CR or LF) will raise ``BadHeaderError``
 
+.. versionchanged:: 3.2
+
+    The :attr:`HttpResponse.headers` interface was added.
+
 Telling the browser to treat the response as a file attachment
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -730,7 +746,7 @@ To tell the browser to treat the response as a file attachment, use the
 this is how you might return a Microsoft Excel spreadsheet::
 
     >>> response = HttpResponse(my_data, content_type='application/vnd.ms-excel')
-    >>> response['Content-Disposition'] = 'attachment; filename="foo.xls"'
+    >>> response.headers['Content-Disposition'] = 'attachment; filename="foo.xls"'
 
 There's nothing Django-specific about the ``Content-Disposition`` header, but
 it's easy to forget the syntax, so we've included it here.
@@ -742,6 +758,13 @@ Attributes
 
     A bytestring representing the content, encoded from a string if necessary.
 
+.. attribute:: HttpResponse.headers
+
+    .. versionadded:: 3.2
+
+    A case insensitive, dict-like object that provides an interface to all
+    HTTP headers on the response. See :ref:`setting-header-fields`.
+
 .. attribute:: HttpResponse.charset
 
     A string denoting the charset in which the response will be encoded. If not

+ 4 - 1
docs/releases/3.2.txt

@@ -309,7 +309,10 @@ Pagination
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* Response headers are now stored in :attr:`.HttpResponse.headers`. This can be
+  used instead of the original dict-like interface of ``HttpResponse`` objects.
+  Both interfaces will continue to be supported. See
+  :ref:`setting-header-fields` for details.
 
 Security
 ~~~~~~~~

+ 4 - 4
docs/topics/cache.txt

@@ -1159,10 +1159,10 @@ In this case, a caching mechanism (such as Django's own cache middleware) will
 cache a separate version of the page for each unique user-agent.
 
 The advantage to using the ``vary_on_headers`` decorator rather than manually
-setting the ``Vary`` header (using something like
-``response['Vary'] = 'user-agent'``) is that the decorator *adds* to the
-``Vary`` header (which may already exist), rather than setting it from scratch
-and potentially overriding anything that was already in there.
+setting the ``Vary`` header (using something like ``response.headers['Vary'] =
+'user-agent'``) is that the decorator *adds* to the ``Vary`` header (which may
+already exist), rather than setting it from scratch and potentially overriding
+anything that was already in there.
 
 You can pass multiple headers to ``vary_on_headers()``::
 

+ 1 - 1
docs/topics/class-based-views/index.txt

@@ -119,7 +119,7 @@ And the view::
             last_book = self.get_queryset().latest('publication_date')
             response = HttpResponse()
             # RFC 1123 date format
-            response['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
+            response.headers['Last-Modified'] = last_book.publication_date.strftime('%a, %d %b %Y %H:%M:%S GMT')
             return response
 
 If the view is accessed from a ``GET`` request, an object list is returned in

+ 3 - 3
docs/topics/testing/tools.txt

@@ -551,9 +551,9 @@ Specifically, a ``Response`` object has the following attributes:
         If the given URL is not found, accessing this attribute will raise a
         :exc:`~django.urls.Resolver404` exception.
 
-You can also use dictionary syntax on the response object to query the value
-of any settings in the HTTP headers. For example, you could determine the
-content type of a response using ``response['Content-Type']``.
+As with a normal response, you can also access the headers through
+:attr:`.HttpResponse.headers`. For example, you could determine the content
+type of a response using ``response.headers['Content-Type']``.
 
 Exceptions
 ----------

+ 3 - 3
tests/admin_docs/test_middleware.py

@@ -13,7 +13,7 @@ class XViewMiddlewareTest(TestDataMixin, AdminDocsTestCase):
         self.client.force_login(self.superuser)
         response = self.client.head('/xview/func/')
         self.assertIn('X-View', response)
-        self.assertEqual(response['X-View'], 'admin_docs.views.xview')
+        self.assertEqual(response.headers['X-View'], 'admin_docs.views.xview')
         user.is_staff = False
         user.save()
         response = self.client.head('/xview/func/')
@@ -31,7 +31,7 @@ class XViewMiddlewareTest(TestDataMixin, AdminDocsTestCase):
         self.client.force_login(self.superuser)
         response = self.client.head('/xview/class/')
         self.assertIn('X-View', response)
-        self.assertEqual(response['X-View'], 'admin_docs.views.XViewClass')
+        self.assertEqual(response.headers['X-View'], 'admin_docs.views.XViewClass')
         user.is_staff = False
         user.save()
         response = self.client.head('/xview/class/')
@@ -45,7 +45,7 @@ class XViewMiddlewareTest(TestDataMixin, AdminDocsTestCase):
     def test_callable_object_view(self):
         self.client.force_login(self.superuser)
         response = self.client.head('/xview/callable_object/')
-        self.assertEqual(response['X-View'], 'admin_docs.views.XViewCallableObject')
+        self.assertEqual(response.headers['X-View'], 'admin_docs.views.XViewCallableObject')
 
     @override_settings(MIDDLEWARE=[])
     def test_no_auth_middleware(self):

+ 1 - 1
tests/admin_views/tests.py

@@ -2964,7 +2964,7 @@ class AdminViewStringPrimaryKeyTest(TestCase):
         )
 
         self.assertEqual(response.status_code, 302)  # temporary redirect
-        self.assertIn('/123_2Fhistory/', response['location'])  # PK is quoted
+        self.assertIn('/123_2Fhistory/', response.headers['location'])  # PK is quoted
 
 
 @override_settings(ROOT_URLCONF='admin_views.urls')

+ 1 - 1
tests/auth_tests/test_views.py

@@ -993,7 +993,7 @@ class LogoutTest(AuthViewsTestCase):
         in #25490.
         """
         response = self.client.get('/logout/')
-        self.assertIn('no-store', response['Cache-Control'])
+        self.assertIn('no-store', response.headers['Cache-Control'])
 
     def test_logout_with_overridden_redirect_url(self):
         # Bug 11223

+ 9 - 9
tests/cache/tests.py

@@ -1700,9 +1700,9 @@ class CacheUtils(SimpleTestCase):
             with self.subTest(initial_vary=initial_vary, newheaders=newheaders):
                 response = HttpResponse()
                 if initial_vary is not None:
-                    response['Vary'] = initial_vary
+                    response.headers['Vary'] = initial_vary
                 patch_vary_headers(response, newheaders)
-                self.assertEqual(response['Vary'], resulting_vary)
+                self.assertEqual(response.headers['Vary'], resulting_vary)
 
     def test_get_cache_key(self):
         request = self.factory.get(self.path)
@@ -1753,7 +1753,7 @@ class CacheUtils(SimpleTestCase):
     def test_learn_cache_key(self):
         request = self.factory.head(self.path)
         response = HttpResponse()
-        response['Vary'] = 'Pony'
+        response.headers['Vary'] = 'Pony'
         # Make sure that the Vary header is added to the key hash
         learn_cache_key(request, response)
 
@@ -1795,9 +1795,9 @@ class CacheUtils(SimpleTestCase):
             with self.subTest(initial_cc=initial_cc, newheaders=newheaders):
                 response = HttpResponse()
                 if initial_cc is not None:
-                    response['Cache-Control'] = initial_cc
+                    response.headers['Cache-Control'] = initial_cc
                 patch_cache_control(response, **newheaders)
-                parts = set(cc_delim_re.split(response['Cache-Control']))
+                parts = set(cc_delim_re.split(response.headers['Cache-Control']))
                 self.assertEqual(parts, expected_cc)
 
 
@@ -1892,7 +1892,7 @@ class CacheI18nTest(SimpleTestCase):
         request.META['HTTP_ACCEPT_LANGUAGE'] = accept_language
         request.META['HTTP_ACCEPT_ENCODING'] = 'gzip;q=1.0, identity; q=0.5, *;q=0'
         response = HttpResponse()
-        response['Vary'] = vary
+        response.headers['Vary'] = vary
         key = learn_cache_key(request, response)
         key2 = get_cache_key(request)
         self.assertEqual(key, reference_key)
@@ -1905,7 +1905,7 @@ class CacheI18nTest(SimpleTestCase):
         request = self.factory.get(self.path)
         request.META['HTTP_ACCEPT_ENCODING'] = 'gzip;q=1.0, identity; q=0.5, *;q=0'
         response = HttpResponse()
-        response['Vary'] = 'accept-encoding'
+        response.headers['Vary'] = 'accept-encoding'
         key = learn_cache_key(request, response)
         self.assertIn(lang, key, "Cache keys should include the language name when translation is active")
         self.check_accept_language_vary(
@@ -2364,9 +2364,9 @@ class TestWithTemplateResponse(SimpleTestCase):
                 template = engines['django'].from_string("This is a test")
                 response = TemplateResponse(HttpRequest(), template)
                 if initial_vary is not None:
-                    response['Vary'] = initial_vary
+                    response.headers['Vary'] = initial_vary
                 patch_vary_headers(response, newheaders)
-                self.assertEqual(response['Vary'], resulting_vary)
+                self.assertEqual(response.headers['Vary'], resulting_vary)
 
     def test_get_cache_key(self):
         request = self.factory.get(self.path)

+ 4 - 4
tests/conditional_processing/tests.py

@@ -21,12 +21,12 @@ class ConditionalGet(SimpleTestCase):
         self.assertEqual(response.content, FULL_RESPONSE.encode())
         if response.request['REQUEST_METHOD'] in ('GET', 'HEAD'):
             if check_last_modified:
-                self.assertEqual(response['Last-Modified'], LAST_MODIFIED_STR)
+                self.assertEqual(response.headers['Last-Modified'], LAST_MODIFIED_STR)
             if check_etag:
-                self.assertEqual(response['ETag'], ETAG)
+                self.assertEqual(response.headers['ETag'], ETAG)
         else:
-            self.assertNotIn('Last-Modified', response)
-            self.assertNotIn('ETag', response)
+            self.assertNotIn('Last-Modified', response.headers)
+            self.assertNotIn('ETag', response.headers)
 
     def assertNotModified(self, response):
         self.assertEqual(response.status_code, 304)

+ 2 - 2
tests/contenttypes_tests/test_views.py

@@ -184,11 +184,11 @@ class ShortcutViewTests(TestCase):
             response = shortcut(self.request, user_ct.id, obj.id)
             self.assertEqual(
                 'http://%s/users/john/' % get_current_site(self.request).domain,
-                response._headers.get('location')[1]
+                response.headers.get('location')
             )
         with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.sites'}):
             response = shortcut(self.request, user_ct.id, obj.id)
-            self.assertEqual('http://Example.com/users/john/', response._headers.get('location')[1])
+            self.assertEqual('http://Example.com/users/john/', response.headers.get('location'))
 
     def test_model_without_get_absolute_url(self):
         """The view returns 404 when Model.get_absolute_url() isn't defined."""

+ 3 - 3
tests/decorators/tests.py

@@ -438,7 +438,7 @@ class XFrameOptionsDecoratorsTests(TestCase):
         def a_view(request):
             return HttpResponse()
         r = a_view(HttpRequest())
-        self.assertEqual(r['X-Frame-Options'], 'DENY')
+        self.assertEqual(r.headers['X-Frame-Options'], 'DENY')
 
     def test_sameorigin_decorator(self):
         """
@@ -449,7 +449,7 @@ class XFrameOptionsDecoratorsTests(TestCase):
         def a_view(request):
             return HttpResponse()
         r = a_view(HttpRequest())
-        self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+        self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
     def test_exempt_decorator(self):
         """
@@ -477,6 +477,6 @@ class NeverCacheDecoratorTest(TestCase):
             return HttpResponse()
         r = a_view(HttpRequest())
         self.assertEqual(
-            set(r['Cache-Control'].split(', ')),
+            set(r.headers['Cache-Control'].split(', ')),
             {'max-age=0', 'no-cache', 'no-store', 'must-revalidate', 'private'},
         )

+ 5 - 5
tests/generic_views/test_base.py

@@ -195,7 +195,7 @@ class ViewTest(SimpleTestCase):
         view = SimpleView.as_view()
         response = view(request)
         self.assertEqual(200, response.status_code)
-        self.assertTrue(response['Allow'])
+        self.assertTrue(response.headers['Allow'])
 
     def test_options_for_get_view(self):
         """
@@ -226,7 +226,7 @@ class ViewTest(SimpleTestCase):
 
     def _assert_allows(self, response, *expected_methods):
         "Assert allowed HTTP methods reported in the Allow response header"
-        response_allows = set(response['Allow'].split(', '))
+        response_allows = set(response.headers['Allow'].split(', '))
         self.assertEqual(set(expected_methods + ('OPTIONS',)), response_allows)
 
     def test_args_kwargs_request_on_self(self):
@@ -390,7 +390,7 @@ class TemplateViewTest(SimpleTestCase):
 
     def test_content_type(self):
         response = self.client.get('/template/content_type/')
-        self.assertEqual(response['Content-Type'], 'text/plain')
+        self.assertEqual(response.headers['Content-Type'], 'text/plain')
 
     def test_resolve_view(self):
         match = resolve('/template/content_type/')
@@ -461,12 +461,12 @@ class RedirectViewTest(SimpleTestCase):
         "Named pattern parameter should reverse to the matching pattern"
         response = RedirectView.as_view(pattern_name='artist_detail')(self.rf.get('/foo/'), pk=1)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['Location'], '/detail/artist/1/')
+        self.assertEqual(response.headers['Location'], '/detail/artist/1/')
 
     def test_named_url_pattern_using_args(self):
         response = RedirectView.as_view(pattern_name='artist_detail')(self.rf.get('/foo/'), 1)
         self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['Location'], '/detail/artist/1/')
+        self.assertEqual(response.headers['Location'], '/detail/artist/1/')
 
     def test_redirect_POST(self):
         "Default is a temporary redirect"

+ 59 - 22
tests/httpwrappers/tests.py

@@ -292,44 +292,44 @@ class HttpResponseTests(unittest.TestCase):
         r = HttpResponse()
 
         # ASCII strings or bytes values are converted to strings.
-        r['key'] = 'test'
-        self.assertEqual(r['key'], 'test')
-        r['key'] = b'test'
-        self.assertEqual(r['key'], 'test')
+        r.headers['key'] = 'test'
+        self.assertEqual(r.headers['key'], 'test')
+        r.headers['key'] = b'test'
+        self.assertEqual(r.headers['key'], 'test')
         self.assertIn(b'test', r.serialize_headers())
 
         # Non-ASCII values are serialized to Latin-1.
-        r['key'] = 'café'
+        r.headers['key'] = 'café'
         self.assertIn('café'.encode('latin-1'), r.serialize_headers())
 
         # Other Unicode values are MIME-encoded (there's no way to pass them as
         # bytes).
-        r['key'] = '†'
-        self.assertEqual(r['key'], '=?utf-8?b?4oCg?=')
+        r.headers['key'] = '†'
+        self.assertEqual(r.headers['key'], '=?utf-8?b?4oCg?=')
         self.assertIn(b'=?utf-8?b?4oCg?=', r.serialize_headers())
 
         # The response also converts string or bytes keys to strings, but requires
         # them to contain ASCII
         r = HttpResponse()
-        del r['Content-Type']
-        r['foo'] = 'bar'
-        headers = list(r.items())
+        del r.headers['Content-Type']
+        r.headers['foo'] = 'bar'
+        headers = list(r.headers.items())
         self.assertEqual(len(headers), 1)
         self.assertEqual(headers[0], ('foo', 'bar'))
 
         r = HttpResponse()
-        del r['Content-Type']
-        r[b'foo'] = 'bar'
-        headers = list(r.items())
+        del r.headers['Content-Type']
+        r.headers[b'foo'] = 'bar'
+        headers = list(r.headers.items())
         self.assertEqual(len(headers), 1)
         self.assertEqual(headers[0], ('foo', 'bar'))
         self.assertIsInstance(headers[0][0], str)
 
         r = HttpResponse()
         with self.assertRaises(UnicodeError):
-            r.__setitem__('føø', 'bar')
+            r.headers.__setitem__('føø', 'bar')
         with self.assertRaises(UnicodeError):
-            r.__setitem__('føø'.encode(), 'bar')
+            r.headers.__setitem__('føø'.encode(), 'bar')
 
     def test_long_line(self):
         # Bug #20889: long lines trigger newlines to be added to headers
@@ -337,18 +337,18 @@ class HttpResponseTests(unittest.TestCase):
         h = HttpResponse()
         f = b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz a\xcc\x88'
         f = f.decode('utf-8')
-        h['Content-Disposition'] = 'attachment; filename="%s"' % f
+        h.headers['Content-Disposition'] = 'attachment; filename="%s"' % f
         # This one is triggering https://bugs.python.org/issue20747, that is Python
         # will itself insert a newline in the header
-        h['Content-Disposition'] = 'attachment; filename="EdelRot_Blu\u0308te (3)-0.JPG"'
+        h.headers['Content-Disposition'] = 'attachment; filename="EdelRot_Blu\u0308te (3)-0.JPG"'
 
     def test_newlines_in_headers(self):
         # Bug #10188: Do not allow newlines in headers (CR or LF)
         r = HttpResponse()
         with self.assertRaises(BadHeaderError):
-            r.__setitem__('test\rstr', 'test')
+            r.headers.__setitem__('test\rstr', 'test')
         with self.assertRaises(BadHeaderError):
-            r.__setitem__('test\nstr', 'test')
+            r.headers.__setitem__('test\nstr', 'test')
 
     def test_dict_behavior(self):
         """
@@ -436,7 +436,7 @@ class HttpResponseTests(unittest.TestCase):
 
         # with Content-Encoding header
         r = HttpResponse()
-        r['Content-Encoding'] = 'winning'
+        r.headers['Content-Encoding'] = 'winning'
         r.write(b'abc')
         r.write(b'def')
         self.assertEqual(r.content, b'abcdef')
@@ -462,6 +462,14 @@ class HttpResponseTests(unittest.TestCase):
             with self.assertRaises(DisallowedRedirect):
                 HttpResponsePermanentRedirect(url)
 
+    def test_header_deletion(self):
+        r = HttpResponse('hello')
+        r.headers['X-Foo'] = 'foo'
+        del r.headers['X-Foo']
+        self.assertNotIn('X-Foo', r.headers)
+        # del doesn't raise a KeyError on nonexistent headers.
+        del r.headers['X-Foo']
+
 
 class HttpResponseSubclassesTests(SimpleTestCase):
     def test_redirect(self):
@@ -474,7 +482,7 @@ class HttpResponseSubclassesTests(SimpleTestCase):
             content_type='text/html',
         )
         self.assertContains(response, 'The resource has temporarily moved', status_code=302)
-        self.assertEqual(response.url, response['Location'])
+        self.assertEqual(response.url, response.headers['Location'])
 
     def test_redirect_lazy(self):
         """Make sure HttpResponseRedirect works with lazy strings."""
@@ -523,7 +531,7 @@ class HttpResponseSubclassesTests(SimpleTestCase):
 
     def test_not_allowed_repr_no_content_type(self):
         response = HttpResponseNotAllowed(('GET', 'POST'))
-        del response['Content-Type']
+        del response.headers['Content-Type']
         self.assertEqual(repr(response), '<HttpResponseNotAllowed [GET, POST] status_code=405>')
 
 
@@ -785,3 +793,32 @@ class CookieTests(unittest.TestCase):
         for proto in range(pickle.HIGHEST_PROTOCOL + 1):
             C1 = pickle.loads(pickle.dumps(C, protocol=proto))
             self.assertEqual(C1.output(), expected_output)
+
+
+class HttpResponseHeadersTestCase(SimpleTestCase):
+    """Headers by treating HttpResponse like a dictionary."""
+    def test_headers(self):
+        response = HttpResponse()
+        response['X-Foo'] = 'bar'
+        self.assertEqual(response['X-Foo'], 'bar')
+        self.assertEqual(response.headers['X-Foo'], 'bar')
+        self.assertIn('X-Foo', response)
+        self.assertIs(response.has_header('X-Foo'), True)
+        del response['X-Foo']
+        self.assertNotIn('X-Foo', response)
+        self.assertNotIn('X-Foo', response.headers)
+        # del doesn't raise a KeyError on nonexistent headers.
+        del response['X-Foo']
+
+    def test_headers_bytestring(self):
+        response = HttpResponse()
+        response['X-Foo'] = b'bar'
+        self.assertEqual(response['X-Foo'], 'bar')
+        self.assertEqual(response.headers['X-Foo'], 'bar')
+
+    def test_newlines_in_headers(self):
+        response = HttpResponse()
+        with self.assertRaises(BadHeaderError):
+            response['test\rstr'] = 'test'
+        with self.assertRaises(BadHeaderError):
+            response['test\nstr'] = 'test'

+ 12 - 12
tests/i18n/patterns/tests.py

@@ -116,7 +116,7 @@ class PathUnusedTests(URLTestCaseBase):
     def test_no_lang_activate(self):
         response = self.client.get('/nl/foo/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-language'], 'en')
+        self.assertEqual(response.headers['content-language'], 'en')
         self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
 
 
@@ -200,7 +200,7 @@ class URLRedirectTests(URLTestCaseBase):
         response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='en')
         self.assertRedirects(response, '/en/account/register/')
 
-        response = self.client.get(response['location'])
+        response = self.client.get(response.headers['location'])
         self.assertEqual(response.status_code, 200)
 
     def test_en_redirect_wrong_url(self):
@@ -211,7 +211,7 @@ class URLRedirectTests(URLTestCaseBase):
         response = self.client.get('/profiel/registreren/', HTTP_ACCEPT_LANGUAGE='nl')
         self.assertRedirects(response, '/nl/profiel/registreren/')
 
-        response = self.client.get(response['location'])
+        response = self.client.get(response.headers['location'])
         self.assertEqual(response.status_code, 200)
 
     def test_nl_redirect_wrong_url(self):
@@ -222,7 +222,7 @@ class URLRedirectTests(URLTestCaseBase):
         response = self.client.get('/conta/registre-se/', HTTP_ACCEPT_LANGUAGE='pt-br')
         self.assertRedirects(response, '/pt-br/conta/registre-se/')
 
-        response = self.client.get(response['location'])
+        response = self.client.get(response.headers['location'])
         self.assertEqual(response.status_code, 200)
 
     def test_pl_pl_redirect(self):
@@ -230,7 +230,7 @@ class URLRedirectTests(URLTestCaseBase):
         response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='pl-pl')
         self.assertRedirects(response, '/en/account/register/')
 
-        response = self.client.get(response['location'])
+        response = self.client.get(response.headers['location'])
         self.assertEqual(response.status_code, 200)
 
     @override_settings(
@@ -258,7 +258,7 @@ class URLVaryAcceptLanguageTests(URLTestCaseBase):
         self.assertRedirects(response, '/en/account/register/')
         self.assertFalse(response.get('Vary'))
 
-        response = self.client.get(response['location'])
+        response = self.client.get(response.headers['location'])
         self.assertEqual(response.status_code, 200)
         self.assertFalse(response.get('Vary'))
 
@@ -297,7 +297,7 @@ class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase):
         response = self.client.get('/account/register-without-slash', HTTP_ACCEPT_LANGUAGE='en')
         self.assertRedirects(response, '/en/account/register-without-slash', 302)
 
-        response = self.client.get(response['location'])
+        response = self.client.get(response.headers['location'])
         self.assertEqual(response.status_code, 200)
 
 
@@ -310,13 +310,13 @@ class URLResponseTests(URLTestCaseBase):
     def test_en_url(self):
         response = self.client.get('/en/account/register/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-language'], 'en')
+        self.assertEqual(response.headers['content-language'], 'en')
         self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
 
     def test_nl_url(self):
         response = self.client.get('/nl/profiel/registreren/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-language'], 'nl')
+        self.assertEqual(response.headers['content-language'], 'nl')
         self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
 
     def test_wrong_en_prefix(self):
@@ -330,19 +330,19 @@ class URLResponseTests(URLTestCaseBase):
     def test_pt_br_url(self):
         response = self.client.get('/pt-br/conta/registre-se/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-language'], 'pt-br')
+        self.assertEqual(response.headers['content-language'], 'pt-br')
         self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br')
 
     def test_en_path(self):
         response = self.client.get('/en/account/register-as-path/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-language'], 'en')
+        self.assertEqual(response.headers['content-language'], 'en')
         self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
 
     def test_nl_path(self):
         response = self.client.get('/nl/profiel/registreren-als-pad/')
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-language'], 'nl')
+        self.assertEqual(response.headers['content-language'], 'nl')
         self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
 
 

+ 46 - 19
tests/middleware/test_security.py

@@ -17,7 +17,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
             response = HttpResponse(*args, **kwargs)
             if headers:
                 for k, v in headers.items():
-                    response[k] = v
+                    response.headers[k] = v
             return response
         return get_response
 
@@ -47,7 +47,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         "Strict-Transport-Security: max-age=3600" to the response.
         """
         self.assertEqual(
-            self.process_response(secure=True)["Strict-Transport-Security"],
+            self.process_response(secure=True).headers['Strict-Transport-Security'],
             'max-age=3600',
         )
 
@@ -60,7 +60,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         response = self.process_response(
             secure=True,
             headers={"Strict-Transport-Security": "max-age=7200"})
-        self.assertEqual(response["Strict-Transport-Security"], "max-age=7200")
+        self.assertEqual(response.headers["Strict-Transport-Security"], "max-age=7200")
 
     @override_settings(SECURE_HSTS_SECONDS=3600)
     def test_sts_only_if_secure(self):
@@ -68,7 +68,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         The "Strict-Transport-Security" header is not added to responses going
         over an insecure connection.
         """
-        self.assertNotIn("Strict-Transport-Security", self.process_response(secure=False))
+        self.assertNotIn(
+            'Strict-Transport-Security',
+            self.process_response(secure=False).headers,
+        )
 
     @override_settings(SECURE_HSTS_SECONDS=0)
     def test_sts_off(self):
@@ -76,7 +79,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         With SECURE_HSTS_SECONDS=0, the middleware does not add a
         "Strict-Transport-Security" header to the response.
         """
-        self.assertNotIn("Strict-Transport-Security", self.process_response(secure=True))
+        self.assertNotIn(
+            'Strict-Transport-Security',
+            self.process_response(secure=True).headers,
+        )
 
     @override_settings(SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=True)
     def test_sts_include_subdomains(self):
@@ -86,7 +92,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         "includeSubDomains" directive to the response.
         """
         response = self.process_response(secure=True)
-        self.assertEqual(response["Strict-Transport-Security"], "max-age=600; includeSubDomains")
+        self.assertEqual(
+            response.headers['Strict-Transport-Security'],
+            'max-age=600; includeSubDomains',
+        )
 
     @override_settings(SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=False)
     def test_sts_no_include_subdomains(self):
@@ -96,7 +105,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         the "includeSubDomains" directive to the response.
         """
         response = self.process_response(secure=True)
-        self.assertEqual(response["Strict-Transport-Security"], "max-age=600")
+        self.assertEqual(response.headers["Strict-Transport-Security"], "max-age=600")
 
     @override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_PRELOAD=True)
     def test_sts_preload(self):
@@ -106,7 +115,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         directive to the response.
         """
         response = self.process_response(secure=True)
-        self.assertEqual(response["Strict-Transport-Security"], "max-age=10886400; preload")
+        self.assertEqual(
+            response.headers['Strict-Transport-Security'],
+            'max-age=10886400; preload',
+        )
 
     @override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_INCLUDE_SUBDOMAINS=True, SECURE_HSTS_PRELOAD=True)
     def test_sts_subdomains_and_preload(self):
@@ -117,7 +129,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         to the response.
         """
         response = self.process_response(secure=True)
-        self.assertEqual(response["Strict-Transport-Security"], "max-age=10886400; includeSubDomains; preload")
+        self.assertEqual(
+            response.headers['Strict-Transport-Security'],
+            'max-age=10886400; includeSubDomains; preload',
+        )
 
     @override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_PRELOAD=False)
     def test_sts_no_preload(self):
@@ -127,7 +142,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         the "preload" directive to the response.
         """
         response = self.process_response(secure=True)
-        self.assertEqual(response["Strict-Transport-Security"], "max-age=10886400")
+        self.assertEqual(
+            response.headers['Strict-Transport-Security'],
+            'max-age=10886400',
+        )
 
     @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True)
     def test_content_type_on(self):
@@ -135,7 +153,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         With SECURE_CONTENT_TYPE_NOSNIFF set to True, the middleware adds
         "X-Content-Type-Options: nosniff" header to the response.
         """
-        self.assertEqual(self.process_response()["X-Content-Type-Options"], "nosniff")
+        self.assertEqual(
+            self.process_response().headers['X-Content-Type-Options'],
+            'nosniff',
+        )
 
     @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True)
     def test_content_type_already_present(self):
@@ -144,7 +165,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         already present in the response.
         """
         response = self.process_response(secure=True, headers={"X-Content-Type-Options": "foo"})
-        self.assertEqual(response["X-Content-Type-Options"], "foo")
+        self.assertEqual(response.headers["X-Content-Type-Options"], "foo")
 
     @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=False)
     def test_content_type_off(self):
@@ -152,7 +173,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         With SECURE_CONTENT_TYPE_NOSNIFF False, the middleware does not add an
         "X-Content-Type-Options" header to the response.
         """
-        self.assertNotIn("X-Content-Type-Options", self.process_response())
+        self.assertNotIn('X-Content-Type-Options', self.process_response().headers)
 
     @override_settings(SECURE_BROWSER_XSS_FILTER=True)
     def test_xss_filter_on(self):
@@ -160,7 +181,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         With SECURE_BROWSER_XSS_FILTER set to True, the middleware adds
         "s-xss-protection: 1; mode=block" header to the response.
         """
-        self.assertEqual(self.process_response()["X-XSS-Protection"], "1; mode=block")
+        self.assertEqual(
+            self.process_response().headers['X-XSS-Protection'],
+            '1; mode=block',
+        )
 
     @override_settings(SECURE_BROWSER_XSS_FILTER=True)
     def test_xss_filter_already_present(self):
@@ -169,7 +193,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         already present in the response.
         """
         response = self.process_response(secure=True, headers={"X-XSS-Protection": "foo"})
-        self.assertEqual(response["X-XSS-Protection"], "foo")
+        self.assertEqual(response.headers["X-XSS-Protection"], "foo")
 
     @override_settings(SECURE_BROWSER_XSS_FILTER=False)
     def test_xss_filter_off(self):
@@ -177,7 +201,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         With SECURE_BROWSER_XSS_FILTER set to False, the middleware does not
         add an "X-XSS-Protection" header to the response.
         """
-        self.assertNotIn("X-XSS-Protection", self.process_response())
+        self.assertNotIn('X-XSS-Protection', self.process_response().headers)
 
     @override_settings(SECURE_SSL_REDIRECT=True)
     def test_ssl_redirect_on(self):
@@ -229,7 +253,7 @@ class SecurityMiddlewareTest(SimpleTestCase):
         With SECURE_REFERRER_POLICY set to None, the middleware does not add a
         "Referrer-Policy" header to the response.
         """
-        self.assertNotIn('Referrer-Policy', self.process_response())
+        self.assertNotIn('Referrer-Policy', self.process_response().headers)
 
     def test_referrer_policy_on(self):
         """
@@ -245,7 +269,10 @@ class SecurityMiddlewareTest(SimpleTestCase):
         )
         for value, expected in tests:
             with self.subTest(value=value), override_settings(SECURE_REFERRER_POLICY=value):
-                self.assertEqual(self.process_response()['Referrer-Policy'], expected)
+                self.assertEqual(
+                    self.process_response().headers['Referrer-Policy'],
+                    expected,
+                )
 
     @override_settings(SECURE_REFERRER_POLICY='strict-origin')
     def test_referrer_policy_already_present(self):
@@ -254,4 +281,4 @@ class SecurityMiddlewareTest(SimpleTestCase):
         present in the response.
         """
         response = self.process_response(headers={'Referrer-Policy': 'unsafe-url'})
-        self.assertEqual(response['Referrer-Policy'], 'unsafe-url')
+        self.assertEqual(response.headers['Referrer-Policy'], 'unsafe-url')

+ 24 - 24
tests/middleware/tests.py

@@ -292,7 +292,7 @@ class CommonMiddlewareTest(SimpleTestCase):
             return response
 
         response = CommonMiddleware(get_response)(self.rf.get('/'))
-        self.assertEqual(int(response['Content-Length']), len(response.content))
+        self.assertEqual(int(response.headers['Content-Length']), len(response.content))
 
     def test_content_length_header_not_added_for_streaming_response(self):
         def get_response(req):
@@ -308,11 +308,11 @@ class CommonMiddlewareTest(SimpleTestCase):
 
         def get_response(req):
             response = HttpResponse()
-            response['Content-Length'] = bad_content_length
+            response.headers['Content-Length'] = bad_content_length
             return response
 
         response = CommonMiddleware(get_response)(self.rf.get('/'))
-        self.assertEqual(int(response['Content-Length']), bad_content_length)
+        self.assertEqual(int(response.headers['Content-Length']), bad_content_length)
 
     # Other tests
 
@@ -607,7 +607,7 @@ class ConditionalGetMiddlewareTest(SimpleTestCase):
         self.assertEqual(new_response.status_code, 304)
         base_response = get_response(self.req)
         for header in ('Cache-Control', 'Content-Location', 'Date', 'ETag', 'Expires', 'Last-Modified', 'Vary'):
-            self.assertEqual(new_response[header], base_response[header])
+            self.assertEqual(new_response.headers[header], base_response.headers[header])
         self.assertEqual(new_response.cookies, base_response.cookies)
         self.assertNotIn('Content-Language', new_response)
 
@@ -622,7 +622,7 @@ class ConditionalGetMiddlewareTest(SimpleTestCase):
             return HttpResponse(status=200)
 
         response = ConditionalGetMiddleware(self.get_response)(self.req)
-        etag = response['ETag']
+        etag = response.headers['ETag']
         put_request = self.request_factory.put('/', HTTP_IF_MATCH=etag)
         conditional_get_response = ConditionalGetMiddleware(get_200_response)(put_request)
         self.assertEqual(conditional_get_response.status_code, 200)  # should never be a 412
@@ -653,11 +653,11 @@ class XFrameOptionsMiddlewareTest(SimpleTestCase):
         """
         with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'):
             r = XFrameOptionsMiddleware(get_response_empty)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+            self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
         with override_settings(X_FRAME_OPTIONS='sameorigin'):
             r = XFrameOptionsMiddleware(get_response_empty)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+            self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
     def test_deny(self):
         """
@@ -666,11 +666,11 @@ class XFrameOptionsMiddlewareTest(SimpleTestCase):
         """
         with override_settings(X_FRAME_OPTIONS='DENY'):
             r = XFrameOptionsMiddleware(get_response_empty)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'DENY')
+            self.assertEqual(r.headers['X-Frame-Options'], 'DENY')
 
         with override_settings(X_FRAME_OPTIONS='deny'):
             r = XFrameOptionsMiddleware(get_response_empty)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'DENY')
+            self.assertEqual(r.headers['X-Frame-Options'], 'DENY')
 
     def test_defaults_sameorigin(self):
         """
@@ -680,7 +680,7 @@ class XFrameOptionsMiddlewareTest(SimpleTestCase):
         with override_settings(X_FRAME_OPTIONS=None):
             del settings.X_FRAME_OPTIONS    # restored by override_settings
             r = XFrameOptionsMiddleware(get_response_empty)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'DENY')
+            self.assertEqual(r.headers['X-Frame-Options'], 'DENY')
 
     def test_dont_set_if_set(self):
         """
@@ -689,21 +689,21 @@ class XFrameOptionsMiddlewareTest(SimpleTestCase):
         """
         def same_origin_response(request):
             response = HttpResponse()
-            response['X-Frame-Options'] = 'SAMEORIGIN'
+            response.headers['X-Frame-Options'] = 'SAMEORIGIN'
             return response
 
         def deny_response(request):
             response = HttpResponse()
-            response['X-Frame-Options'] = 'DENY'
+            response.headers['X-Frame-Options'] = 'DENY'
             return response
 
         with override_settings(X_FRAME_OPTIONS='DENY'):
             r = XFrameOptionsMiddleware(same_origin_response)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+            self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
         with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'):
             r = XFrameOptionsMiddleware(deny_response)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'DENY')
+            self.assertEqual(r.headers['X-Frame-Options'], 'DENY')
 
     def test_response_exempt(self):
         """
@@ -722,10 +722,10 @@ class XFrameOptionsMiddlewareTest(SimpleTestCase):
 
         with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'):
             r = XFrameOptionsMiddleware(xframe_not_exempt_response)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+            self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
             r = XFrameOptionsMiddleware(xframe_exempt_response)(HttpRequest())
-            self.assertIsNone(r.get('X-Frame-Options'))
+            self.assertIsNone(r.headers.get('X-Frame-Options'))
 
     def test_is_extendable(self):
         """
@@ -749,16 +749,16 @@ class XFrameOptionsMiddlewareTest(SimpleTestCase):
 
         with override_settings(X_FRAME_OPTIONS='DENY'):
             r = OtherXFrameOptionsMiddleware(same_origin_response)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+            self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
             request = HttpRequest()
             request.sameorigin = True
             r = OtherXFrameOptionsMiddleware(get_response_empty)(request)
-            self.assertEqual(r['X-Frame-Options'], 'SAMEORIGIN')
+            self.assertEqual(r.headers['X-Frame-Options'], 'SAMEORIGIN')
 
         with override_settings(X_FRAME_OPTIONS='SAMEORIGIN'):
             r = OtherXFrameOptionsMiddleware(get_response_empty)(HttpRequest())
-            self.assertEqual(r['X-Frame-Options'], 'DENY')
+            self.assertEqual(r.headers['X-Frame-Options'], 'DENY')
 
 
 class GZipMiddlewareTest(SimpleTestCase):
@@ -916,12 +916,12 @@ class ETagGZipMiddlewareTest(SimpleTestCase):
         """
         def get_response(req):
             response = HttpResponse(self.compressible_string)
-            response['ETag'] = '"eggs"'
+            response.headers['ETag'] = '"eggs"'
             return response
 
         request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate')
         gzip_response = GZipMiddleware(get_response)(request)
-        self.assertEqual(gzip_response['ETag'], 'W/"eggs"')
+        self.assertEqual(gzip_response.headers['ETag'], 'W/"eggs"')
 
     def test_weak_etag_not_modified(self):
         """
@@ -929,12 +929,12 @@ class ETagGZipMiddlewareTest(SimpleTestCase):
         """
         def get_response(req):
             response = HttpResponse(self.compressible_string)
-            response['ETag'] = 'W/"eggs"'
+            response.headers['ETag'] = 'W/"eggs"'
             return response
 
         request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate')
         gzip_response = GZipMiddleware(get_response)(request)
-        self.assertEqual(gzip_response['ETag'], 'W/"eggs"')
+        self.assertEqual(gzip_response.headers['ETag'], 'W/"eggs"')
 
     def test_etag_match(self):
         """
@@ -949,7 +949,7 @@ class ETagGZipMiddlewareTest(SimpleTestCase):
 
         request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate')
         response = GZipMiddleware(get_cond_response)(request)
-        gzip_etag = response['ETag']
+        gzip_etag = response.headers['ETag']
         next_request = self.rf.get('/', HTTP_ACCEPT_ENCODING='gzip, deflate', HTTP_IF_NONE_MATCH=gzip_etag)
         next_response = ConditionalGetMiddleware(get_response)(next_request)
         self.assertEqual(next_response.status_code, 304)

+ 23 - 14
tests/responses/test_fileresponse.py

@@ -12,23 +12,26 @@ from django.test import SimpleTestCase
 class FileResponseTests(SimpleTestCase):
     def test_file_from_disk_response(self):
         response = FileResponse(open(__file__, 'rb'))
-        self.assertEqual(response['Content-Length'], str(os.path.getsize(__file__)))
-        self.assertIn(response['Content-Type'], ['text/x-python', 'text/plain'])
-        self.assertEqual(response['Content-Disposition'], 'inline; filename="test_fileresponse.py"')
+        self.assertEqual(response.headers['Content-Length'], str(os.path.getsize(__file__)))
+        self.assertIn(response.headers['Content-Type'], ['text/x-python', 'text/plain'])
+        self.assertEqual(
+            response.headers['Content-Disposition'],
+            'inline; filename="test_fileresponse.py"',
+        )
         response.close()
 
     def test_file_from_buffer_response(self):
         response = FileResponse(io.BytesIO(b'binary content'))
-        self.assertEqual(response['Content-Length'], '14')
-        self.assertEqual(response['Content-Type'], 'application/octet-stream')
+        self.assertEqual(response.headers['Content-Length'], '14')
+        self.assertEqual(response.headers['Content-Type'], 'application/octet-stream')
         self.assertFalse(response.has_header('Content-Disposition'))
         self.assertEqual(list(response), [b'binary content'])
 
     def test_file_from_buffer_unnamed_attachment(self):
         response = FileResponse(io.BytesIO(b'binary content'), as_attachment=True)
-        self.assertEqual(response['Content-Length'], '14')
-        self.assertEqual(response['Content-Type'], 'application/octet-stream')
-        self.assertEqual(response['Content-Disposition'], 'attachment')
+        self.assertEqual(response.headers['Content-Length'], '14')
+        self.assertEqual(response.headers['Content-Type'], 'application/octet-stream')
+        self.assertEqual(response.headers['Content-Disposition'], 'attachment')
         self.assertEqual(list(response), [b'binary content'])
 
     @skipIf(sys.platform == 'win32', "Named pipes are Unix-only.")
@@ -47,9 +50,12 @@ class FileResponseTests(SimpleTestCase):
 
     def test_file_from_disk_as_attachment(self):
         response = FileResponse(open(__file__, 'rb'), as_attachment=True)
-        self.assertEqual(response['Content-Length'], str(os.path.getsize(__file__)))
-        self.assertIn(response['Content-Type'], ['text/x-python', 'text/plain'])
-        self.assertEqual(response['Content-Disposition'], 'attachment; filename="test_fileresponse.py"')
+        self.assertEqual(response.headers['Content-Length'], str(os.path.getsize(__file__)))
+        self.assertIn(response.headers['Content-Type'], ['text/x-python', 'text/plain'])
+        self.assertEqual(
+            response.headers['Content-Disposition'],
+            'attachment; filename="test_fileresponse.py"',
+        )
         response.close()
 
     def test_compressed_response(self):
@@ -67,7 +73,7 @@ class FileResponseTests(SimpleTestCase):
             with self.subTest(ext=extension):
                 with tempfile.NamedTemporaryFile(suffix=extension) as tmp:
                     response = FileResponse(tmp)
-                self.assertEqual(response['Content-Type'], mimetype)
+                self.assertEqual(response.headers['Content-Type'], mimetype)
                 self.assertFalse(response.has_header('Content-Encoding'))
 
     def test_unicode_attachment(self):
@@ -75,8 +81,11 @@ class FileResponseTests(SimpleTestCase):
             ContentFile(b'binary content', name="祝您平安.odt"), as_attachment=True,
             content_type='application/vnd.oasis.opendocument.text',
         )
-        self.assertEqual(response['Content-Type'], 'application/vnd.oasis.opendocument.text')
         self.assertEqual(
-            response['Content-Disposition'],
+            response.headers['Content-Type'],
+            'application/vnd.oasis.opendocument.text',
+        )
+        self.assertEqual(
+            response.headers['Content-Disposition'],
             "attachment; filename*=utf-8''%E7%A5%9D%E6%82%A8%E5%B9%B3%E5%AE%89.odt"
         )

+ 5 - 5
tests/responses/tests.py

@@ -39,12 +39,12 @@ class HttpResponseBaseTests(SimpleTestCase):
         """
         r = HttpResponseBase()
 
-        r['Header'] = 'Value'
+        r.headers['Header'] = 'Value'
         r.setdefault('header', 'changed')
-        self.assertEqual(r['header'], 'Value')
+        self.assertEqual(r.headers['header'], 'Value')
 
         r.setdefault('x-header', 'DefaultValue')
-        self.assertEqual(r['X-Header'], 'DefaultValue')
+        self.assertEqual(r.headers['X-Header'], 'DefaultValue')
 
 
 class HttpResponseTests(SimpleTestCase):
@@ -92,7 +92,7 @@ class HttpResponseTests(SimpleTestCase):
 
         response = HttpResponse(charset=ISO88591)
         self.assertEqual(response.charset, ISO88591)
-        self.assertEqual(response['Content-Type'], 'text/html; charset=%s' % ISO88591)
+        self.assertEqual(response.headers['Content-Type'], 'text/html; charset=%s' % ISO88591)
 
         response = HttpResponse(content_type='text/plain; charset=%s' % UTF8, charset=ISO88591)
         self.assertEqual(response.charset, ISO88591)
@@ -134,7 +134,7 @@ class HttpResponseTests(SimpleTestCase):
 
     def test_repr_no_content_type(self):
         response = HttpResponse(status=204)
-        del response['Content-Type']
+        del response.headers['Content-Type']
         self.assertEqual(repr(response), '<HttpResponse status_code=204>')
 
     def test_wrap_textiowrapper(self):

+ 4 - 4
tests/sessions_tests/tests.py

@@ -781,7 +781,7 @@ class SessionMiddlewareTests(TestCase):
         )
         # SessionMiddleware sets 'Vary: Cookie' to prevent the 'Set-Cookie'
         # from being cached.
-        self.assertEqual(response['Vary'], 'Cookie')
+        self.assertEqual(response.headers['Vary'], 'Cookie')
 
     @override_settings(SESSION_COOKIE_DOMAIN='.example.local', SESSION_COOKIE_PATH='/example/')
     def test_session_delete_on_end_with_custom_domain_and_path(self):
@@ -826,7 +826,7 @@ class SessionMiddlewareTests(TestCase):
         # A cookie should not be set.
         self.assertEqual(response.cookies, {})
         # The session is accessed so "Vary: Cookie" should be set.
-        self.assertEqual(response['Vary'], 'Cookie')
+        self.assertEqual(response.headers['Vary'], 'Cookie')
 
     def test_empty_session_saved(self):
         """
@@ -849,7 +849,7 @@ class SessionMiddlewareTests(TestCase):
             'Set-Cookie: sessionid=%s' % request.session.session_key,
             str(response.cookies)
         )
-        self.assertEqual(response['Vary'], 'Cookie')
+        self.assertEqual(response.headers['Vary'], 'Cookie')
 
         # Empty the session data.
         del request.session['foo']
@@ -866,7 +866,7 @@ class SessionMiddlewareTests(TestCase):
             'Set-Cookie: sessionid=%s' % request.session.session_key,
             str(response.cookies)
         )
-        self.assertEqual(response['Vary'], 'Cookie')
+        self.assertEqual(response.headers['Vary'], 'Cookie')
 
 
 class CookieSessionTests(SessionTestsMixin, SimpleTestCase):

+ 2 - 2
tests/shortcuts/tests.py

@@ -9,7 +9,7 @@ class RenderTests(SimpleTestCase):
         response = self.client.get('/render/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.content, b'FOO.BAR../render/\n')
-        self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
+        self.assertEqual(response.headers['Content-Type'], 'text/html; charset=utf-8')
         self.assertFalse(hasattr(response.context.request, 'current_app'))
 
     def test_render_with_multiple_templates(self):
@@ -21,7 +21,7 @@ class RenderTests(SimpleTestCase):
         response = self.client.get('/render/content_type/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.content, b'FOO.BAR../render/content_type/\n')
-        self.assertEqual(response['Content-Type'], 'application/x-rendertest')
+        self.assertEqual(response.headers['Content-Type'], 'application/x-rendertest')
 
     def test_render_with_status(self):
         response = self.client.get('/render/status/')

+ 1 - 1
tests/sitemaps_tests/test_generic.py

@@ -56,4 +56,4 @@ class GenericViewsSitemapTests(SitemapTestsBase):
 </urlset>
 """ % (self.base_url, test_model.pk)
         self.assertXMLEqual(response.content.decode(), expected_content)
-        self.assertEqual(response['Last-Modified'], 'Wed, 13 Mar 2013 10:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Wed, 13 Mar 2013 10:00:00 GMT')

+ 7 - 7
tests/sitemaps_tests/test_http.py

@@ -116,14 +116,14 @@ class HTTPSitemapTests(SitemapTestsBase):
     def test_sitemap_last_modified(self):
         "Last-Modified header is set correctly"
         response = self.client.get('/lastmod/sitemap.xml')
-        self.assertEqual(response['Last-Modified'], 'Wed, 13 Mar 2013 10:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Wed, 13 Mar 2013 10:00:00 GMT')
 
     def test_sitemap_last_modified_date(self):
         """
         The Last-Modified header should be support dates (without time).
         """
         response = self.client.get('/lastmod/date-sitemap.xml')
-        self.assertEqual(response['Last-Modified'], 'Wed, 13 Mar 2013 00:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Wed, 13 Mar 2013 00:00:00 GMT')
 
     def test_sitemap_last_modified_tz(self):
         """
@@ -131,7 +131,7 @@ class HTTPSitemapTests(SitemapTestsBase):
         to GMT.
         """
         response = self.client.get('/lastmod/tz-sitemap.xml')
-        self.assertEqual(response['Last-Modified'], 'Wed, 13 Mar 2013 15:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Wed, 13 Mar 2013 15:00:00 GMT')
 
     def test_sitemap_last_modified_missing(self):
         "Last-Modified header is missing when sitemap has no lastmod"
@@ -165,7 +165,7 @@ class HTTPSitemapTests(SitemapTestsBase):
         Test sitemaps are sorted by lastmod in ascending order.
         """
         response = self.client.get('/lastmod-sitemaps/ascending.xml')
-        self.assertEqual(response['Last-Modified'], 'Sat, 20 Apr 2013 05:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Sat, 20 Apr 2013 05:00:00 GMT')
 
     def test_sitemaps_lastmod_descending(self):
         """
@@ -173,7 +173,7 @@ class HTTPSitemapTests(SitemapTestsBase):
         Test sitemaps are sorted by lastmod in descending order.
         """
         response = self.client.get('/lastmod-sitemaps/descending.xml')
-        self.assertEqual(response['Last-Modified'], 'Sat, 20 Apr 2013 05:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Sat, 20 Apr 2013 05:00:00 GMT')
 
     @override_settings(USE_I18N=True, USE_L10N=True)
     def test_localized_priority(self):
@@ -243,10 +243,10 @@ class HTTPSitemapTests(SitemapTestsBase):
 
     def test_x_robots_sitemap(self):
         response = self.client.get('/simple/index.xml')
-        self.assertEqual(response['X-Robots-Tag'], 'noindex, noodp, noarchive')
+        self.assertEqual(response.headers['X-Robots-Tag'], 'noindex, noodp, noarchive')
 
         response = self.client.get('/simple/sitemap.xml')
-        self.assertEqual(response['X-Robots-Tag'], 'noindex, noodp, noarchive')
+        self.assertEqual(response.headers['X-Robots-Tag'], 'noindex, noodp, noarchive')
 
     def test_empty_sitemap(self):
         response = self.client.get('/empty/sitemap.xml')

+ 2 - 2
tests/syndication_tests/tests.py

@@ -421,14 +421,14 @@ class SyndicationFeedTest(FeedTestCase):
         Tests the Last-Modified header with naive publication dates.
         """
         response = self.client.get('/syndication/naive-dates/')
-        self.assertEqual(response['Last-Modified'], 'Tue, 26 Mar 2013 01:00:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Tue, 26 Mar 2013 01:00:00 GMT')
 
     def test_feed_last_modified_time(self):
         """
         Tests the Last-Modified header with aware publication dates.
         """
         response = self.client.get('/syndication/aware-dates/')
-        self.assertEqual(response['Last-Modified'], 'Mon, 25 Mar 2013 19:18:00 GMT')
+        self.assertEqual(response.headers['Last-Modified'], 'Mon, 25 Mar 2013 19:18:00 GMT')
 
         # No last-modified when feed has no item_pubdate
         response = self.client.get('/syndication/no_pubdate/')

+ 6 - 6
tests/template_tests/test_response.py

@@ -122,13 +122,13 @@ class SimpleTemplateResponseTest(SimpleTestCase):
 
     def test_kwargs(self):
         response = self._response(content_type='application/json', status=504, charset='ascii')
-        self.assertEqual(response['content-type'], 'application/json')
+        self.assertEqual(response.headers['content-type'], 'application/json')
         self.assertEqual(response.status_code, 504)
         self.assertEqual(response.charset, 'ascii')
 
     def test_args(self):
         response = SimpleTemplateResponse('', {}, 'application/json', 504)
-        self.assertEqual(response['content-type'], 'application/json')
+        self.assertEqual(response.headers['content-type'], 'application/json')
         self.assertEqual(response.status_code, 504)
 
     @require_jinja2
@@ -175,7 +175,7 @@ class SimpleTemplateResponseTest(SimpleTestCase):
         unpickled_response = pickle.loads(pickled_response)
 
         self.assertEqual(unpickled_response.content, response.content)
-        self.assertEqual(unpickled_response['content-type'], response['content-type'])
+        self.assertEqual(unpickled_response.headers['content-type'], response.headers['content-type'])
         self.assertEqual(unpickled_response.status_code, response.status_code)
 
         # ...and the unpickled response doesn't have the
@@ -249,13 +249,13 @@ class TemplateResponseTest(SimpleTestCase):
 
     def test_kwargs(self):
         response = self._response(content_type='application/json', status=504)
-        self.assertEqual(response['content-type'], 'application/json')
+        self.assertEqual(response.headers['content-type'], 'application/json')
         self.assertEqual(response.status_code, 504)
 
     def test_args(self):
         response = TemplateResponse(self.factory.get('/'), '', {},
                                     'application/json', 504)
-        self.assertEqual(response['content-type'], 'application/json')
+        self.assertEqual(response.headers['content-type'], 'application/json')
         self.assertEqual(response.status_code, 504)
 
     @require_jinja2
@@ -287,7 +287,7 @@ class TemplateResponseTest(SimpleTestCase):
         unpickled_response = pickle.loads(pickled_response)
 
         self.assertEqual(unpickled_response.content, response.content)
-        self.assertEqual(unpickled_response['content-type'], response['content-type'])
+        self.assertEqual(unpickled_response.headers['content-type'], response.headers['content-type'])
         self.assertEqual(unpickled_response.status_code, response.status_code)
 
         # ...and the unpickled response doesn't have the

+ 1 - 1
tests/test_client/tests.py

@@ -159,7 +159,7 @@ class ClientTest(TestCase):
         "Check the value of HTTP headers returned in a response"
         response = self.client.get("/header_view/")
 
-        self.assertEqual(response['X-DJANGO-TEST'], 'Slartibartfast')
+        self.assertEqual(response.headers['X-DJANGO-TEST'], 'Slartibartfast')
 
     def test_response_attached_request(self):
         """

+ 1 - 1
tests/test_client/views.py

@@ -102,7 +102,7 @@ def json_view(request):
 def view_with_header(request):
     "A view that has a custom header"
     response = HttpResponse()
-    response['X-DJANGO-TEST'] = 'Slartibartfast'
+    response.headers['X-DJANGO-TEST'] = 'Slartibartfast'
     return response
 
 

+ 1 - 1
tests/test_client_regress/tests.py

@@ -1210,7 +1210,7 @@ class RequestMethodStringDataTests(SimpleTestCase):
         )
         for content_type in valid_types:
             response = self.client.get('/json_response/', {'content_type': content_type})
-            self.assertEqual(response['Content-Type'], content_type)
+            self.assertEqual(response.headers['Content-Type'], content_type)
             self.assertEqual(response.json(), {'key': 'value'})
 
     def test_json_multiple_access(self):

+ 1 - 1
tests/view_tests/tests/test_debug.py

@@ -1481,7 +1481,7 @@ class NonHTMLResponseExceptionReporterFilter(ExceptionReportTestMixin, LoggingCa
     @override_settings(DEBUG=True, ROOT_URLCONF='view_tests.urls')
     def test_non_html_response_encoding(self):
         response = self.client.get('/raises500/', HTTP_ACCEPT='application/json')
-        self.assertEqual(response['Content-Type'], 'text/plain; charset=utf-8')
+        self.assertEqual(response.headers['Content-Type'], 'text/plain; charset=utf-8')
 
 
 class DecoratorsTests(SimpleTestCase):

+ 1 - 1
tests/view_tests/tests/test_i18n.py

@@ -249,7 +249,7 @@ class I18NViewTests(SimpleTestCase):
                 catalog = gettext.translation('djangojs', locale_dir, [lang_code])
                 trans_txt = catalog.gettext('this is to be translated')
                 response = self.client.get('/jsi18n/')
-                self.assertEqual(response['Content-Type'], 'text/javascript; charset="utf-8"')
+                self.assertEqual(response.headers['Content-Type'], 'text/javascript; charset="utf-8"')
                 # response content must include a line like:
                 # "this is to be translated": <value of trans_txt Python variable>
                 # json.dumps() is used to be able to check Unicode strings.

+ 1 - 1
tests/view_tests/tests/test_json.py

@@ -10,7 +10,7 @@ class JsonResponseTests(SimpleTestCase):
         response = self.client.get('/json/response/')
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
-            response['content-type'], 'application/json')
+            response.headers['content-type'], 'application/json')
         self.assertEqual(json.loads(response.content.decode()), {
             'a': [1, 2, 3],
             'foo': {'bar': 'baz'},

+ 4 - 4
tests/view_tests/tests/test_static.py

@@ -29,7 +29,7 @@ class StaticTests(SimpleTestCase):
             file_path = path.join(media_dir, filename)
             with open(file_path, 'rb') as fp:
                 self.assertEqual(fp.read(), response_content)
-            self.assertEqual(len(response_content), int(response['Content-Length']))
+            self.assertEqual(len(response_content), int(response.headers['Content-Length']))
             self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None))
 
     def test_chunked(self):
@@ -44,7 +44,7 @@ class StaticTests(SimpleTestCase):
 
     def test_unknown_mime_type(self):
         response = self.client.get('/%s/file.unknown' % self.prefix)
-        self.assertEqual('application/octet-stream', response['Content-Type'])
+        self.assertEqual('application/octet-stream', response.headers['Content-Type'])
         response.close()
 
     def test_copes_with_empty_path_component(self):
@@ -87,7 +87,7 @@ class StaticTests(SimpleTestCase):
         response_content = b''.join(response)
         with open(path.join(media_dir, file_name), 'rb') as fp:
             self.assertEqual(fp.read(), response_content)
-        self.assertEqual(len(response_content), int(response['Content-Length']))
+        self.assertEqual(len(response_content), int(response.headers['Content-Length']))
 
     def test_invalid_if_modified_since2(self):
         """Handle even more bogus If-Modified-Since values gracefully
@@ -102,7 +102,7 @@ class StaticTests(SimpleTestCase):
         response_content = b''.join(response)
         with open(path.join(media_dir, file_name), 'rb') as fp:
             self.assertEqual(fp.read(), response_content)
-        self.assertEqual(len(response_content), int(response['Content-Length']))
+        self.assertEqual(len(response_content), int(response.headers['Content-Length']))
 
     def test_404(self):
         response = self.client.get('/%s/nonexistent_resource' % self.prefix)