1
0
mirror of https://github.com/django/django.git synced 2025-07-04 01:39:20 +00:00

[soc2009/http-wsgi-improvements] Change HttpResponse.status_code to a property, additional test coverage. Refs #10190.

Improve charset coverage. Change HttpResponse.status_code to property, which checks for a 406 situation. This also required changes to all HttpResponse subclasses, so that their default status_code is set by _status_code, now.

Passes regression test suite, except ones related to sendfile.

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/http-wsgi-improvements@11199 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Chris Cahoon 2009-07-06 20:06:11 +00:00
parent 4d46aed514
commit 8d979aecbe
5 changed files with 116 additions and 100 deletions

View File

@ -13,7 +13,7 @@ except ImportError:
from django.utils.datastructures import MultiValueDict, ImmutableList from django.utils.datastructures import MultiValueDict, ImmutableList
from django.utils.encoding import smart_str, iri_to_uri, force_unicode from django.utils.encoding import smart_str, iri_to_uri, force_unicode
from django.http.multipartparser import MultiPartParser from django.http.multipartparser import MultiPartParser
from django.http.charsets import determine_charset, get_codec from django.http.charsets import get_response_encoding, get_codec
from django.conf import settings from django.conf import settings
from django.core.files import uploadhandler from django.core.files import uploadhandler
from utils import * from utils import *
@ -270,24 +270,21 @@ class BadHeaderError(ValueError):
class HttpResponse(object): class HttpResponse(object):
"""A basic HTTP response, with content and dictionary-accessed headers.""" """A basic HTTP response, with content and dictionary-accessed headers."""
status_code = 200 _status_code = 200
_codec = None
_charset = settings.DEFAULT_CHARSET
def __init__(self, content='', mimetype=None, status=None, def __init__(self, content='', mimetype=None, status=None,
content_type=None, request=None): content_type=None, request=None):
from django.conf import settings from django.conf import settings
self._charset = settings.DEFAULT_CHARSET
self._codec = None
accept_charset = None accept_charset = None
if mimetype: if mimetype:
content_type = mimetype # Mimetype is an alias for content-type content_type = mimetype # Mimetype arg is an alias for content-type
if request: if request:
accept_charset = request.META.get("ACCEPT_CHARSET") accept_charset = request.META.get("ACCEPT_CHARSET")
if accept_charset or content_type: if accept_charset or content_type:
charset, codec = determine_charset(content_type, accept_charset) encoding = get_response_encoding(content_type, accept_charset)
if charset: (self._charset, self._codec) = encoding
self._charset = charset
if codec:
self._codec = codec
if not content_type: if not content_type:
content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
self._charset) self._charset)
@ -370,12 +367,27 @@ class HttpResponse(object):
self.set_cookie(key, max_age=0, path=path, domain=domain, self.set_cookie(key, max_age=0, path=path, domain=domain,
expires='Thu, 01-Jan-1970 00:00:00 GMT') expires='Thu, 01-Jan-1970 00:00:00 GMT')
def _get_status_code(self):
if not self._valid_codec():
self._status_code = 406
self._container = ['']
return self._status_code
def _set_status_code(self, value):
self._status_code = value
status_code = property(_get_status_code, _set_status_code)
def _valid_codec(self):
if not self._codec:
self._codec = get_codec(self._charset)
if not self._codec:
return False
return True
def _get_content(self): def _get_content(self):
if self.has_header('Content-Encoding'): if self.has_header('Content-Encoding'):
return ''.join(self._container) return ''.join(self._container)
if not self._codec:
self._codec = get_codec(self._charset)
return smart_str(''.join(self._container), self._codec.name) return smart_str(''.join(self._container), self._codec.name)
def _set_content(self, value): def _set_content(self, value):
@ -390,8 +402,6 @@ class HttpResponse(object):
def next(self): def next(self):
chunk = self._iterator.next() chunk = self._iterator.next()
if not self._codec:
self._codec = get_codec(self._charset)
if isinstance(chunk, unicode): if isinstance(chunk, unicode):
chunk = chunk.encode(self._codec.name) chunk = chunk.encode(self._codec.name)
return str(chunk) return str(chunk)
@ -432,57 +442,57 @@ class HttpResponseSendFile(HttpResponse):
self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file
def _get_content(self): def _get_content(self):
return open(self.sendfile_filename) return open(self.sendfile_filename).read()
content = property(_get_content) content = property(_get_content)
class HttpResponseRedirect(HttpResponse): class HttpResponseRedirect(HttpResponse):
status_code = 302 _status_code = 302
def __init__(self, redirect_to): def __init__(self, redirect_to):
HttpResponse.__init__(self) HttpResponse.__init__(self)
self['Location'] = redirect_to self['Location'] = redirect_to
class HttpResponsePermanentRedirect(HttpResponse): class HttpResponsePermanentRedirect(HttpResponse):
status_code = 301 _status_code = 301
def __init__(self, redirect_to): def __init__(self, redirect_to):
HttpResponse.__init__(self) HttpResponse.__init__(self)
self['Location'] = redirect_to self['Location'] = redirect_to
class HttpResponseNotModified(HttpResponse): class HttpResponseNotModified(HttpResponse):
status_code = 304 _status_code = 304
class HttpResponseBadRequest(HttpResponse): class HttpResponseBadRequest(HttpResponse):
status_code = 400 _status_code = 400
class HttpResponseNotFound(HttpResponse): class HttpResponseNotFound(HttpResponse):
status_code = 404 _status_code = 404
class HttpResponseForbidden(HttpResponse): class HttpResponseForbidden(HttpResponse):
status_code = 403 _status_code = 403
class HttpResponseNotAllowed(HttpResponse): class HttpResponseNotAllowed(HttpResponse):
status_code = 405 _status_code = 405
def __init__(self, permitted_methods): def __init__(self, permitted_methods):
HttpResponse.__init__(self) HttpResponse.__init__(self)
self['Allow'] = ', '.join(permitted_methods) self['Allow'] = ', '.join(permitted_methods)
class HttpResponseNotAcceptable(HttpResponse): class HttpResponseNotAcceptable(HttpResponse):
status_code = 406 _status_code = 406
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
# if we want to make this more verbose (compliant, actually) # if we want to make this more verbose (compliant, actually)
class HttpResponseGone(HttpResponse): class HttpResponseGone(HttpResponse):
status_code = 410 _status_code = 410
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
HttpResponse.__init__(self, *args, **kwargs) HttpResponse.__init__(self, *args, **kwargs)
class HttpResponseServerError(HttpResponse): class HttpResponseServerError(HttpResponse):
status_code = 500 _status_code = 500
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
HttpResponse.__init__(self, *args, **kwargs) HttpResponse.__init__(self, *args, **kwargs)

View File

@ -235,16 +235,14 @@ def get_codec(charset):
CODEC_CHARSETS above has the codecs that correspond to character sets. CODEC_CHARSETS above has the codecs that correspond to character sets.
""" """
try: codec = None
codec_name = CHARSET_CODECS[charset.strip().lower()] if charset:
codec = codecs.lookup(codec_name) try:
except KeyError: codec_name = CHARSET_CODECS[charset.strip().lower()]
#print "The charset %s is not supported by Django." % charset codec = codecs.lookup(codec_name)
codec = None except LookupError:
except LookupError: # The encoding is not supported in this version of Python.
#print "The encoding '%s' is not supported in this version of Python." % codec_name pass
codec = None
return codec return codec
# Returns the key for the maximum value in a dictionary # Returns the key for the maximum value in a dictionary
@ -252,7 +250,7 @@ max_dict_key = lambda l:sorted(l.iteritems(), key=itemgetter(1), reverse=True)[0
CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?') CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?')
ACCEPT_CHARSET_RE = re.compile('(?P<charset>([\w\d-]+)|(\*))(;q=(?P<q>[01](\.\d{1,3})?))?,?') ACCEPT_CHARSET_RE = re.compile('(?P<charset>([\w\d-]+)|(\*))(;q=(?P<q>[01](\.\d{1,3})?))?,?')
def determine_charset(content_type, accept_charset_header): def get_response_encoding(content_type, accept_charset_header):
""" """
Searches request headers from clients and mimetype settings (which may be set Searches request headers from clients and mimetype settings (which may be set
by users) for indicators of which charset and encoding the response should use. by users) for indicators of which charset and encoding the response should use.
@ -268,56 +266,54 @@ def determine_charset(content_type, accept_charset_header):
406 error 406 error
""" """
codec = None used_content_type = False
charset = None charset = None
# Attempt to get the codec from a content-type, and verify that the charset is valid. codec = None
# Try to get the codec from a content-type, verify that the charset is valid.
if content_type: if content_type:
match = CONTENT_TYPE_RE.match(content_type) match = CONTENT_TYPE_RE.match(content_type)
if match: if match:
charset = match.group(1) charset = match.group(1)
codec = get_codec(charset) codec = get_codec(charset)
if not codec: # Unsupported charset if not codec: # Unsupported charset
# we should throw an exception here raise Exception("Unsupported charset in Content-Type header.")
# print "No CODEC ON MIMETYPE"
pass
# If we don't match a content-type header WITH charset, we give the default
else: else:
charset = settings.DEFAULT_CHARSET charset = settings.DEFAULT_CHARSET
codec = get_codec(settings.DEFAULT_CHARSET) used_content_type = True
# Handle Accept-Charset (which we only do if we do not deal with content_type). # Handle Accept-Charset (only if we have not gotten one with content_type).
else: if not used_content_type:
if accept_charset_header: if not accept_charset_header: # No information to find a charset with.
# Get list of matches for Accepted-Charsets. return None, None
# [{ charset : q }, { charset : q }] # Get list of matches for Accepted-Charsets.
match_iterator = ACCEPT_CHARSET_RE.finditer(accept_charset_header) # [{ charset : q }, { charset : q }]
accept_charset = [m.groupdict() for m in match_iterator] match_iterator = ACCEPT_CHARSET_RE.finditer(accept_charset_header)
else: accept_charset = [m.groupdict() for m in match_iterator]
accept_charset = [] # use settings.DEFAULT_CHARSET
charset = settings.DEFAULT_CHARSET
# Remove charsets we cannot encode and whose q values are 0 # Remove charsets we cannot encode and whose q values are 0
charsets = _process_accept_charset(accept_charset) charsets = _process_accept_charset(accept_charset)
# If we did not get a charset from the content type, we get it from accept_charset. # Establish the prioritized charsets (ones we know about beforehand)
if not charset: default_charset = settings.DEFAULT_CHARSET
default_charset = settings.DEFAULT_CHARSET fallback_charset = "ISO-8859-1"
fallback_charset = "ISO-8859-1"
# Prefer default_charset if its q value is 1 or we have no valid acceptable charsets. # Prefer default_charset if its q value is 1 or we have no valid acceptable charsets.
max_q_charset = max_dict_key(charsets) max_q_charset = max_dict_key(charsets)
max_q_value = charsets[max_q_charset] max_q_value = charsets[max_q_charset]
if max_q_value == 0 and fallback_charset not in charsets: if max_q_value == 0:
if fallback_charset not in charsets or charsets[fallback_charset] > 0:
charset = fallback_charset charset = fallback_charset
elif charsets[default_charset] == 1 or charsets[default_charset] == max_q_value: elif charsets[default_charset] == 1 or charsets[default_charset] == max_q_value:
charset = default_charset charset = default_charset
# Get the highest valued acceptable charset (if we aren't going to the fallback # Get the highest valued acceptable charset (if we aren't going to the fallback
# or defaulting) # or defaulting)
else: else:
charset = max_q_charset charset = max_q_charset
codec = get_codec(charset) codec = get_codec(charset)
# We may reach here with no codec or no charset. We will change the status # We may reach here with no codec or no charset. We will change the status
# code in the HttpResponse. # code in the HttpResponse.
#print charset, codec
return charset, codec return charset, codec
# NOTE -- make sure we are not duping the processing of q values # NOTE -- make sure we are not duping the processing of q values
@ -352,4 +348,4 @@ def _process_accept_charset(accept_charset):
accepted_charsets["ISO-8859-1"] = default_value accepted_charsets["ISO-8859-1"] = default_value
return accepted_charsets return accepted_charsets

View File

@ -2,12 +2,12 @@ import re
from django.test import Client, TestCase from django.test import Client, TestCase
from django.conf import settings from django.conf import settings
from django.http.charsets import determine_charset, get_codec from django.http.charsets import get_response_encoding, get_codec
CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?') CHARSET_RE = re.compile('.*; charset=([\w\d-]+);?')
def get_charset(response): def get_charset(response):
match = CONTENT_TYPE_RE.match(response.get("content-type","")) match = CHARSET_RE.match(response.get("content-type",""))
if match: if match:
charset = match.group(1) charset = match.group(1)
else: else:
@ -18,7 +18,7 @@ class ClientTest(TestCase):
urls = 'regressiontests.charsets.urls' urls = 'regressiontests.charsets.urls'
def test_good_accept_charset(self): def test_good_accept_charset(self):
"Use Accept-Charset" "Use Accept-Charset, with a quality value that throws away default_charset"
# The data is ignored, but let's check it doesn't crash the system # The data is ignored, but let's check it doesn't crash the system
# anyway. # anyway.
@ -27,61 +27,67 @@ class ClientTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), "ascii") self.assertEqual(get_charset(response), "ascii")
def test_good_accept_charset2(self): def test_quality_sorting_wildcard_wins(self):
# us is an alias for ascii
response = self.client.post('/accept_charset/', ACCEPT_CHARSET="us;q=0.8,*;q=0.9") response = self.client.post('/accept_charset/', ACCEPT_CHARSET="us;q=0.8,*;q=0.9")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET) self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET)
def test_good_accept_charset3(self): def test_quality_sorting_wildcard_loses_alias_wins(self):
# us is an alias for ascii
response = self.client.post('/accept_charset/', ACCEPT_CHARSET="us;q=0.8,*;q=0.7") response = self.client.post('/accept_charset/', ACCEPT_CHARSET="us;q=0.8,*;q=0.7")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), "us") self.assertEqual(get_charset(response), "us")
def test_good_accept_charset4(self): def test_quality_sorting(self):
response = self.client.post('/accept_charset/', ACCEPT_CHARSET="ascii;q=0.89,utf-8;q=.9") response = self.client.post('/accept_charset/', ACCEPT_CHARSET="ascii;q=0.89,utf-8;q=.9")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET) self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET)
def test_good_accept_charset5(self): def test_fallback_charset(self):
response = self.client.post('/accept_charset/', ACCEPT_CHARSET="utf-8;q=0") response = self.client.post('/accept_charset/', ACCEPT_CHARSET="utf-8;q=0")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), "ISO-8859-1") self.assertEqual(get_charset(response), "ISO-8859-1")
def test_bad_accept_charset(self): def test_bad_accept_charset(self):
"Do not use a malformed Accept-Charset" "Do not use a charset that Python does not support"
# The data is ignored, but let's check it doesn't crash the system
# anyway. response = self.client.post('/accept_charset/', ACCEPT_CHARSET="Huttese")
response = self.client.post('/accept_charset/', ACCEPT_CHARSET="this_is_junk")
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), "utf-8") self.assertEqual(get_charset(response), "utf-8")
def test_force_no_charset(self):
"If we have no accepted charsets that we have codecs for, 406"
response = self.client.post('/accept_charset/', ACCEPT_CHARSET="utf-8;q=0,*;q=0")
self.assertEqual(response.status_code, 406)
def test_good_content_type(self): def test_good_content_type(self):
"Use good content-type" "Use good content-type"
# The data is ignored, but let's check it doesn't crash the system
# anyway.
response = self.client.post('/good_content_type/') response = self.client.post('/good_content_type/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), "us")
def test_bad_content_type(self): def test_bad_content_type(self):
"Use bad content-type" "Use bad content-type"
self.assertRaises(Exception, self.client.get, "/bad_content_type/")
response = self.client.post('/bad_content_type/')
self.assertEqual(response.status_code, 200)
self.assertEqual(get_codec(get_charset(response)), None)
def test_content_type_no_charset(self): def test_content_type_no_charset(self):
response = self.client.post('/content_type_no_charset/') response = self.client.post('/content_type_no_charset/')
self.assertEqual(get_charset(response), None) self.assertEqual(get_charset(response), None)
def test_determine_charset(self): def test_determine_charset(self):
content_type, codec = determine_charset("", "utf-8;q=0.8,*;q=0.9") content_type, codec = get_response_encoding("", "utf-8;q=0.8,*;q=0.9")
self.assertEqual(codec, get_codec("ISO-8859-1")) self.assertEqual(codec, get_codec("ISO-8859-1"))
def test_basic_response(self):
"Make sure a normal request gets the default charset, with a 200 response."
response = self.client.post('/basic_response/')
self.assertEqual(response.status_code, 200)
self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET)

View File

@ -19,4 +19,5 @@ urlpatterns = patterns('',
(r'^good_content_type/', views.good_content_type), (r'^good_content_type/', views.good_content_type),
(r'^bad_content_type/', views.bad_content_type), (r'^bad_content_type/', views.bad_content_type),
(r'^content_type_no_charset/', views.content_type_no_charset), (r'^content_type_no_charset/', views.content_type_no_charset),
(r'^basic_response/', views.basic_response),
) )

View File

@ -14,4 +14,7 @@ def content_type_no_charset(request):
return HttpResponse("UTF-8", content_type="text/html") return HttpResponse("UTF-8", content_type="text/html")
def encode_response(request): def encode_response(request):
return HttpResponse(u"\ue863", content_type="text/html; charset=GBK") return HttpResponse(u"\ue863", content_type="text/html; charset=GBK")
def basic_response(request):
return HttpResponse("ASCII.")