From bcc2befd0e9c1885e45b46d0b0bcdc11def8b249 Mon Sep 17 00:00:00 2001 From: Tom Carrick Date: Tue, 14 Jul 2020 13:32:24 +0200 Subject: [PATCH] Fixed #31789 -- Added a new headers interface to HttpResponse. --- django/contrib/admin/tests.py | 2 +- django/contrib/admindocs/middleware.py | 2 +- django/contrib/sitemaps/views.py | 4 +- django/contrib/syndication/views.py | 2 +- django/http/response.py | 128 +++++++++++++++--------- django/middleware/clickjacking.py | 6 +- django/middleware/common.py | 2 +- django/middleware/gzip.py | 8 +- django/middleware/locale.py | 2 +- django/middleware/security.py | 8 +- django/utils/cache.py | 22 ++-- django/views/decorators/http.py | 4 +- django/views/generic/base.py | 4 +- django/views/static.py | 4 +- docs/howto/outputting-csv.txt | 6 +- docs/ref/request-response.txt | 31 +++++- docs/releases/3.2.txt | 5 +- docs/topics/cache.txt | 8 +- docs/topics/class-based-views/index.txt | 2 +- docs/topics/testing/tools.txt | 6 +- tests/admin_docs/test_middleware.py | 6 +- tests/admin_views/tests.py | 2 +- tests/auth_tests/test_views.py | 2 +- tests/cache/tests.py | 18 ++-- tests/conditional_processing/tests.py | 8 +- tests/contenttypes_tests/test_views.py | 4 +- tests/decorators/tests.py | 6 +- tests/generic_views/test_base.py | 10 +- tests/httpwrappers/tests.py | 81 +++++++++++---- tests/i18n/patterns/tests.py | 24 ++--- tests/middleware/test_security.py | 65 ++++++++---- tests/middleware/tests.py | 48 ++++----- tests/responses/test_fileresponse.py | 37 ++++--- tests/responses/tests.py | 10 +- tests/sessions_tests/tests.py | 8 +- tests/shortcuts/tests.py | 4 +- tests/sitemaps_tests/test_generic.py | 2 +- tests/sitemaps_tests/test_http.py | 14 +-- tests/syndication_tests/tests.py | 4 +- tests/template_tests/test_response.py | 12 +-- tests/test_client/tests.py | 2 +- tests/test_client/views.py | 2 +- tests/test_client_regress/tests.py | 2 +- tests/view_tests/tests/test_debug.py | 2 +- tests/view_tests/tests/test_i18n.py | 2 +- tests/view_tests/tests/test_json.py | 2 +- tests/view_tests/tests/test_static.py | 8 +- 47 files changed, 385 insertions(+), 256 deletions(-) diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index 941d030d30..482027b1ae 100644 --- a/django/contrib/admin/tests.py +++ b/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 diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py index 77d77af715..4779db8366 100644 --- a/django/contrib/admindocs/middleware.py +++ b/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 diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py index ab6b9a6d24..bffdebb082 100644 --- a/django/contrib/sitemaps/views.py +++ b/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 diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index df97103318..6d567dd7db 100644 --- a/django/contrib/syndication/views.py +++ b/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 diff --git a/django/http/response.py b/django/http/response.py index 64ac205087..e679c856c0 100644 --- a/django/http/response.py +++ b/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[^\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): diff --git a/django/middleware/clickjacking.py b/django/middleware/clickjacking.py index 478ed3cd7e..0161f8eb8f 100644 --- a/django/middleware/clickjacking.py +++ b/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): diff --git a/django/middleware/common.py b/django/middleware/common.py index 7e75e81303..e6f30f44ad 100644 --- a/django/middleware/common.py +++ b/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 diff --git a/django/middleware/gzip.py b/django/middleware/gzip.py index 14346c5b12..350466151d 100644 --- a/django/middleware/gzip.py +++ b/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 diff --git a/django/middleware/locale.py b/django/middleware/locale.py index e4f3537320..0bbdda3309 100644 --- a/django/middleware/locale.py +++ b/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 diff --git a/django/middleware/security.py b/django/middleware/security.py index 44921cd22b..d923893dc5 100644 --- a/django/middleware/security.py +++ b/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 )) diff --git a/django/utils/cache.py b/django/utils/cache.py index 72f017f38a..0541d373ee 100644 --- a/django/utils/cache.py +++ b/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) diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index 673302be83..5caf13e341 100644 --- a/django/views/decorators/http.py +++ b/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 diff --git a/django/views/generic/base.py b/django/views/generic/base.py index 3dd957d8f8..ab800ebce8 100644 --- a/django/views/generic/base.py +++ b/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): diff --git a/django/views/static.py b/django/views/static.py index 18e137ce7b..1d4900b1da 100644 --- a/django/views/static.py +++ b/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 diff --git a/docs/howto/outputting-csv.txt b/docs/howto/outputting-csv.txt index 2886a1b294..dc3cf57cfd 100644 --- a/docs/howto/outputting-csv.txt +++ b/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. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 6b3ec54dd5..40063becc9 100644 --- a/docs/ref/request-response.txt +++ b/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 diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 9000b170eb..30cc6a401a 100644 --- a/docs/releases/3.2.txt +++ b/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 ~~~~~~~~ diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index ccaa82277f..4e1b2546ad 100644 --- a/docs/topics/cache.txt +++ b/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()``:: diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt index 3ec00f7361..8874545469 100644 --- a/docs/topics/class-based-views/index.txt +++ b/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 diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 741acd604c..6d96731c79 100644 --- a/docs/topics/testing/tools.txt +++ b/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 ---------- diff --git a/tests/admin_docs/test_middleware.py b/tests/admin_docs/test_middleware.py index 7c89dce929..9f5f19fa32 100644 --- a/tests/admin_docs/test_middleware.py +++ b/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): diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 61894de9f0..89ae04ff56 100644 --- a/tests/admin_views/tests.py +++ b/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') diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index bb887e49ea..9d669c5d85 100644 --- a/tests/auth_tests/test_views.py +++ b/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 diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 93f0d87ecb..1c51c38b2b 100644 --- a/tests/cache/tests.py +++ b/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) diff --git a/tests/conditional_processing/tests.py b/tests/conditional_processing/tests.py index 349b1cf7fe..4c7a32cba2 100644 --- a/tests/conditional_processing/tests.py +++ b/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) diff --git a/tests/contenttypes_tests/test_views.py b/tests/contenttypes_tests/test_views.py index 4c654658ce..e54e5ff925 100644 --- a/tests/contenttypes_tests/test_views.py +++ b/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.""" diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index 6f1f02b1af..5bb21661e3 100644 --- a/tests/decorators/tests.py +++ b/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'}, ) diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 7aaea3ffa0..f4b8a487ff 100644 --- a/tests/generic_views/test_base.py +++ b/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" diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 04c2a9516c..3c648f6b31 100644 --- a/tests/httpwrappers/tests.py +++ b/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), '') @@ -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' diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py index b561b04fb3..96e9453e9e 100644 --- a/tests/i18n/patterns/tests.py +++ b/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') diff --git a/tests/middleware/test_security.py b/tests/middleware/test_security.py index d907c25166..d766643b9b 100644 --- a/tests/middleware/test_security.py +++ b/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') diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 14c9284bbf..4b49858cd9 100644 --- a/tests/middleware/tests.py +++ b/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) diff --git a/tests/responses/test_fileresponse.py b/tests/responses/test_fileresponse.py index e77df4513a..46d407bdf5 100644 --- a/tests/responses/test_fileresponse.py +++ b/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" ) diff --git a/tests/responses/tests.py b/tests/responses/tests.py index 2c161ee352..059cdf86ed 100644 --- a/tests/responses/tests.py +++ b/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), '') def test_wrap_textiowrapper(self): diff --git a/tests/sessions_tests/tests.py b/tests/sessions_tests/tests.py index 11adef9d75..2832fd8970 100644 --- a/tests/sessions_tests/tests.py +++ b/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): diff --git a/tests/shortcuts/tests.py b/tests/shortcuts/tests.py index fe68d67767..000b9eca00 100644 --- a/tests/shortcuts/tests.py +++ b/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/') diff --git a/tests/sitemaps_tests/test_generic.py b/tests/sitemaps_tests/test_generic.py index 0efdf4e61d..b1ec2fed35 100644 --- a/tests/sitemaps_tests/test_generic.py +++ b/tests/sitemaps_tests/test_generic.py @@ -56,4 +56,4 @@ class GenericViewsSitemapTests(SitemapTestsBase): """ % (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') diff --git a/tests/sitemaps_tests/test_http.py b/tests/sitemaps_tests/test_http.py index 3281774cc5..b546c87fe6 100644 --- a/tests/sitemaps_tests/test_http.py +++ b/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') diff --git a/tests/syndication_tests/tests.py b/tests/syndication_tests/tests.py index d9456ed618..b763bba6cb 100644 --- a/tests/syndication_tests/tests.py +++ b/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/') diff --git a/tests/template_tests/test_response.py b/tests/template_tests/test_response.py index cf5e955223..0a51d68f01 100644 --- a/tests/template_tests/test_response.py +++ b/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 diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 03bb658952..f0b7f798a0 100644 --- a/tests/test_client/tests.py +++ b/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): """ diff --git a/tests/test_client/views.py b/tests/test_client/views.py index 034ca6908c..38fbeae797 100644 --- a/tests/test_client/views.py +++ b/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 diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index cb6cb4a8ac..91cf23f0cb 100644 --- a/tests/test_client_regress/tests.py +++ b/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): diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index 2337d3ed3d..80f5af89f7 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/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): diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index fcbffa711d..0ec2bf877e 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/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": # json.dumps() is used to be able to check Unicode strings. diff --git a/tests/view_tests/tests/test_json.py b/tests/view_tests/tests/test_json.py index 34e8fa359b..e1074bf630 100644 --- a/tests/view_tests/tests/test_json.py +++ b/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'}, diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py index f4c58e0611..5044aca2d6 100644 --- a/tests/view_tests/tests/test_static.py +++ b/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)