Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[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
  • Loading branch information...
commit 8d979aecbeeac180a4e21922df68b9f41ba2fa50 1 parent 4d46aed
Christopher Cahoon authored July 06, 2009
62  django/http/__init__.py
@@ -13,7 +13,7 @@
13 13
 from django.utils.datastructures import MultiValueDict, ImmutableList
14 14
 from django.utils.encoding import smart_str, iri_to_uri, force_unicode
15 15
 from django.http.multipartparser import MultiPartParser
16  
-from django.http.charsets import determine_charset, get_codec
  16
+from django.http.charsets import get_response_encoding, get_codec
17 17
 from django.conf import settings
18 18
 from django.core.files import uploadhandler
19 19
 from utils import *
@@ -270,24 +270,21 @@ class BadHeaderError(ValueError):
270 270
 class HttpResponse(object):
271 271
     """A basic HTTP response, with content and dictionary-accessed headers."""
272 272
 
273  
-    status_code = 200
  273
+    _status_code = 200
  274
+    _codec = None
  275
+    _charset = settings.DEFAULT_CHARSET
274 276
 
275 277
     def __init__(self, content='', mimetype=None, status=None,
276 278
             content_type=None, request=None):
277 279
         from django.conf import settings
278  
-        self._charset = settings.DEFAULT_CHARSET
279  
-        self._codec = None
280 280
         accept_charset = None
281 281
         if mimetype:
282  
-            content_type = mimetype     # Mimetype is an alias for content-type 
  282
+            content_type = mimetype  # Mimetype arg is an alias for content-type
283 283
         if request:
284 284
             accept_charset = request.META.get("ACCEPT_CHARSET")
285 285
         if accept_charset or content_type:
286  
-            charset, codec = determine_charset(content_type, accept_charset)
287  
-            if charset:
288  
-                self._charset = charset
289  
-            if codec:
290  
-                self._codec = codec
  286
+            encoding = get_response_encoding(content_type, accept_charset)
  287
+            (self._charset, self._codec) = encoding
291 288
         if not content_type:
292 289
             content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE,
293 290
                     self._charset)
@@ -370,12 +367,27 @@ def delete_cookie(self, key, path='/', domain=None):
370 367
         self.set_cookie(key, max_age=0, path=path, domain=domain,
371 368
                         expires='Thu, 01-Jan-1970 00:00:00 GMT')
372 369
 
  370
+    def _get_status_code(self):
  371
+        if not self._valid_codec():
  372
+            self._status_code = 406
  373
+            self._container = ['']
  374
+        return self._status_code
  375
+
  376
+    def _set_status_code(self, value):
  377
+        self._status_code = value
  378
+
  379
+    status_code = property(_get_status_code, _set_status_code)
  380
+
  381
+    def _valid_codec(self):
  382
+        if not self._codec:
  383
+            self._codec = get_codec(self._charset)
  384
+        if not self._codec:
  385
+            return False
  386
+        return True
  387
+
373 388
     def _get_content(self):
374 389
         if self.has_header('Content-Encoding'):
375 390
             return ''.join(self._container)
376  
-        
377  
-        if not self._codec:
378  
-            self._codec = get_codec(self._charset)
379 391
         return smart_str(''.join(self._container), self._codec.name)
380 392
 
381 393
     def _set_content(self, value):
@@ -390,8 +402,6 @@ def __iter__(self):
390 402
 
391 403
     def next(self):
392 404
         chunk = self._iterator.next()
393  
-        if not self._codec:
394  
-            self._codec = get_codec(self._charset)
395 405
         if isinstance(chunk, unicode):
396 406
             chunk = chunk.encode(self._codec.name)
397 407
         return str(chunk)
@@ -432,57 +442,57 @@ def __init__(self, path_to_file, content_type=None, block_size=8192):
432 442
  	    self[settings.HTTPRESPONSE_SENDFILE_HEADER] = path_to_file 
433 443
 
434 444
     def _get_content(self):
435  
-        return open(self.sendfile_filename)
  445
+        return open(self.sendfile_filename).read()
436 446
 
437 447
     content = property(_get_content)
438 448
 
439 449
 class HttpResponseRedirect(HttpResponse):
440  
-    status_code = 302
  450
+    _status_code = 302
441 451
 
442 452
     def __init__(self, redirect_to):
443 453
         HttpResponse.__init__(self)
444 454
         self['Location'] = redirect_to
445 455
 
446 456
 class HttpResponsePermanentRedirect(HttpResponse):
447  
-    status_code = 301
  457
+    _status_code = 301
448 458
 
449 459
     def __init__(self, redirect_to):
450 460
         HttpResponse.__init__(self)
451 461
         self['Location'] = redirect_to
452 462
 
453 463
 class HttpResponseNotModified(HttpResponse):
454  
-    status_code = 304
  464
+    _status_code = 304
455 465
 
456 466
 class HttpResponseBadRequest(HttpResponse):
457  
-    status_code = 400
  467
+    _status_code = 400
458 468
 
459 469
 class HttpResponseNotFound(HttpResponse):
460  
-    status_code = 404
  470
+    _status_code = 404
461 471
 
462 472
 class HttpResponseForbidden(HttpResponse):
463  
-    status_code = 403
  473
+    _status_code = 403
464 474
 
465 475
 class HttpResponseNotAllowed(HttpResponse):
466  
-    status_code = 405
  476
+    _status_code = 405
467 477
 
468 478
     def __init__(self, permitted_methods):
469 479
         HttpResponse.__init__(self)
470 480
         self['Allow'] = ', '.join(permitted_methods)
471 481
 
472 482
 class HttpResponseNotAcceptable(HttpResponse):
473  
-    status_code = 406
  483
+    _status_code = 406
474 484
 
475 485
     # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
476 486
     # if we want to make this more verbose (compliant, actually)
477 487
 
478 488
 class HttpResponseGone(HttpResponse):
479  
-    status_code = 410
  489
+    _status_code = 410
480 490
 
481 491
     def __init__(self, *args, **kwargs):
482 492
         HttpResponse.__init__(self, *args, **kwargs)
483 493
 
484 494
 class HttpResponseServerError(HttpResponse):
485  
-    status_code = 500
  495
+    _status_code = 500
486 496
 
487 497
     def __init__(self, *args, **kwargs):
488 498
         HttpResponse.__init__(self, *args, **kwargs)
88  django/http/charsets.py
@@ -235,16 +235,14 @@ def get_codec(charset):
235 235
     
236 236
     CODEC_CHARSETS above has the codecs that correspond to character sets.
237 237
     """
238  
-    try:
239  
-        codec_name = CHARSET_CODECS[charset.strip().lower()]
240  
-        codec = codecs.lookup(codec_name)     
241  
-    except KeyError:
242  
-        #print "The charset %s is not supported by Django." % charset
243  
-        codec = None
244  
-    except LookupError:
245  
-        #print "The encoding '%s' is not supported in this version of Python." % codec_name
246  
-        codec = None 
247  
-    
  238
+    codec = None
  239
+    if charset:
  240
+        try:
  241
+            codec_name = CHARSET_CODECS[charset.strip().lower()]
  242
+            codec = codecs.lookup(codec_name)
  243
+        except LookupError:
  244
+            # The encoding is not supported in this version of Python.
  245
+            pass
248 246
     return codec
249 247
 
250 248
 # Returns the key for the maximum value in a dictionary
@@ -252,7 +250,7 @@ def get_codec(charset):
252 250
 
253 251
 CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?')
254 252
 ACCEPT_CHARSET_RE = re.compile('(?P<charset>([\w\d-]+)|(\*))(;q=(?P<q>[01](\.\d{1,3})?))?,?')
255  
-def determine_charset(content_type, accept_charset_header):
  253
+def get_response_encoding(content_type, accept_charset_header):
256 254
     """
257 255
     Searches request headers from clients and mimetype settings (which may be set 
258 256
     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):
268 266
                 406 error
269 267
             
270 268
     """
271  
-    codec = None
  269
+    used_content_type = False
272 270
     charset = None
273  
-    # Attempt to get the codec from a content-type, and verify that the charset is valid.
  271
+    codec = None
  272
+    # Try to get the codec from a content-type, verify that the charset is valid.
274 273
     if content_type:
275 274
         match = CONTENT_TYPE_RE.match(content_type)
276 275
         if match:
277 276
             charset = match.group(1)
278 277
             codec = get_codec(charset)
279 278
             if not codec:   # Unsupported charset
280  
-                # we should throw an exception here
281  
-                # print "No CODEC ON MIMETYPE"
282  
-                pass
283  
-        # If we don't match a content-type header WITH charset, we give the default
284  
-        else:
285  
-            charset = settings.DEFAULT_CHARSET
286  
-            codec = get_codec(settings.DEFAULT_CHARSET)
287  
-    
288  
-    # Handle Accept-Charset (which we only do if we do not deal with content_type).
289  
-    else:
290  
-        if accept_charset_header:
291  
-            # Get list of matches for Accepted-Charsets.
292  
-            # [{ charset : q }, { charset : q }]
293  
-            match_iterator = ACCEPT_CHARSET_RE.finditer(accept_charset_header)
294  
-            accept_charset = [m.groupdict() for m in match_iterator]
  279
+                raise Exception("Unsupported charset in Content-Type header.")
295 280
         else:
296  
-            accept_charset = []    # use settings.DEFAULT_CHARSET
297 281
             charset = settings.DEFAULT_CHARSET
298  
-            
  282
+        used_content_type = True
  283
+
  284
+    # Handle Accept-Charset (only if we have not gotten one with content_type).
  285
+    if not used_content_type:
  286
+        if not accept_charset_header: # No information to find a charset with.
  287
+            return None, None
  288
+        # Get list of matches for Accepted-Charsets.
  289
+        # [{ charset : q }, { charset : q }]
  290
+        match_iterator = ACCEPT_CHARSET_RE.finditer(accept_charset_header)
  291
+        accept_charset = [m.groupdict() for m in match_iterator]
  292
+
299 293
         # Remove charsets we cannot encode and whose q values are 0
300 294
         charsets = _process_accept_charset(accept_charset)
301 295
         
302  
-        # If we did not get a charset from the content type, we get it from accept_charset.
303  
-        if not charset:
304  
-            default_charset = settings.DEFAULT_CHARSET
305  
-            fallback_charset = "ISO-8859-1"
306  
-            # Prefer default_charset if its q value is 1 or we have no valid acceptable charsets.
307  
-            max_q_charset = max_dict_key(charsets)
308  
-            max_q_value = charsets[max_q_charset]
309  
-            if max_q_value == 0 and fallback_charset not in charsets:
  296
+        # Establish the prioritized charsets (ones we know about beforehand)
  297
+        default_charset = settings.DEFAULT_CHARSET
  298
+        fallback_charset = "ISO-8859-1"
  299
+
  300
+        # Prefer default_charset if its q value is 1 or we have no valid acceptable charsets.
  301
+        max_q_charset = max_dict_key(charsets)
  302
+        max_q_value = charsets[max_q_charset]
  303
+        if max_q_value == 0:
  304
+            if fallback_charset not in charsets or charsets[fallback_charset] > 0:
310 305
                 charset = fallback_charset
311  
-            elif charsets[default_charset] == 1 or charsets[default_charset] == max_q_value:
312  
-                charset = default_charset
313  
-            # Get the highest valued acceptable charset (if we aren't going to the fallback
314  
-            # or defaulting)
315  
-            else:
316  
-                charset = max_q_charset
  306
+        elif charsets[default_charset] == 1 or charsets[default_charset] == max_q_value:
  307
+            charset = default_charset
  308
+        # Get the highest valued acceptable charset (if we aren't going to the fallback
  309
+        # or defaulting)
  310
+        else:
  311
+            charset = max_q_charset
317 312
             
318  
-        codec = get_codec(charset)
  313
+    codec = get_codec(charset)
319 314
     # We may reach here with no codec or no charset. We will change the status 
320 315
     # code in the HttpResponse.
  316
+    #print charset, codec
321 317
     return charset, codec
322 318
 
323 319
 # NOTE -- make sure we are not duping the processing of q values
@@ -352,4 +348,4 @@ def _process_accept_charset(accept_charset):
352 348
         accepted_charsets["ISO-8859-1"] = default_value
353 349
       
354 350
       
355  
-    return accepted_charsets
  351
+    return accepted_charsets
60  tests/regressiontests/charsets/tests.py
@@ -2,12 +2,12 @@
2 2
 
3 3
 from django.test import Client, TestCase
4 4
 from django.conf import settings
5  
-from django.http.charsets import determine_charset, get_codec
  5
+from django.http.charsets import get_response_encoding, get_codec
6 6
 
7 7
 
8  
-CONTENT_TYPE_RE = re.compile('.*; charset=([\w\d-]+);?')
  8
+CHARSET_RE = re.compile('.*; charset=([\w\d-]+);?')
9 9
 def get_charset(response):
10  
-    match = CONTENT_TYPE_RE.match(response.get("content-type",""))
  10
+    match = CHARSET_RE.match(response.get("content-type",""))
11 11
     if match:
12 12
         charset = match.group(1)
13 13
     else:
@@ -18,7 +18,7 @@ class ClientTest(TestCase):
18 18
     urls = 'regressiontests.charsets.urls'
19 19
     
20 20
     def test_good_accept_charset(self):
21  
-        "Use Accept-Charset"
  21
+        "Use Accept-Charset, with a quality value that throws away default_charset"
22 22
         # The data is ignored, but let's check it doesn't crash the system
23 23
         # anyway.
24 24
         
@@ -27,61 +27,67 @@ def test_good_accept_charset(self):
27 27
         self.assertEqual(response.status_code, 200)
28 28
         self.assertEqual(get_charset(response), "ascii")
29 29
     
30  
-    def test_good_accept_charset2(self):
31  
-        # us is an alias for ascii
  30
+    def test_quality_sorting_wildcard_wins(self):
32 31
         response = self.client.post('/accept_charset/', ACCEPT_CHARSET="us;q=0.8,*;q=0.9")
33 32
         
34 33
         self.assertEqual(response.status_code, 200)
35 34
         self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET)
36 35
     
37  
-    def test_good_accept_charset3(self):     
  36
+    def test_quality_sorting_wildcard_loses_alias_wins(self):
  37
+        # us is an alias for ascii
38 38
         response = self.client.post('/accept_charset/', ACCEPT_CHARSET="us;q=0.8,*;q=0.7")
39 39
         
40 40
         self.assertEqual(response.status_code, 200)
41 41
         self.assertEqual(get_charset(response), "us")
42 42
     
43  
-    def test_good_accept_charset4(self):
  43
+    def test_quality_sorting(self):
44 44
         response = self.client.post('/accept_charset/', ACCEPT_CHARSET="ascii;q=0.89,utf-8;q=.9")
45 45
         
46 46
         self.assertEqual(response.status_code, 200)
47 47
         self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET)
48 48
     
49  
-    def test_good_accept_charset5(self):    
  49
+    def test_fallback_charset(self):
50 50
         response = self.client.post('/accept_charset/', ACCEPT_CHARSET="utf-8;q=0")
51 51
         
52 52
         self.assertEqual(response.status_code, 200)
53 53
         self.assertEqual(get_charset(response), "ISO-8859-1")  
54 54
         
55 55
     def test_bad_accept_charset(self):
56  
-        "Do not use a malformed Accept-Charset"
57  
-        # The data is ignored, but let's check it doesn't crash the system
58  
-        # anyway.
59  
-        
60  
-        response = self.client.post('/accept_charset/', ACCEPT_CHARSET="this_is_junk")
  56
+        "Do not use a charset that Python does not support"
  57
+
  58
+        response = self.client.post('/accept_charset/', ACCEPT_CHARSET="Huttese")
61 59
         
62 60
         self.assertEqual(response.status_code, 200)
63 61
         self.assertEqual(get_charset(response), "utf-8")
64  
-        
  62
+
  63
+    def test_force_no_charset(self):
  64
+        "If we have no accepted charsets that we have codecs for, 406"
  65
+        response = self.client.post('/accept_charset/', ACCEPT_CHARSET="utf-8;q=0,*;q=0")
  66
+
  67
+        self.assertEqual(response.status_code, 406)
  68
+
65 69
     def test_good_content_type(self):
66 70
         "Use good content-type"
67  
-        # The data is ignored, but let's check it doesn't crash the system
68  
-        # anyway.
69  
-        
  71
+
70 72
         response = self.client.post('/good_content_type/')
71 73
         self.assertEqual(response.status_code, 200)
72  
-        
  74
+        self.assertEqual(get_charset(response), "us")
  75
+
73 76
     def test_bad_content_type(self):
74 77
         "Use bad content-type"
75  
-        
76  
-        response = self.client.post('/bad_content_type/')
77  
-        self.assertEqual(response.status_code, 200)
78  
-        self.assertEqual(get_codec(get_charset(response)), None)
79  
-    
  78
+        self.assertRaises(Exception, self.client.get, "/bad_content_type/")
  79
+
80 80
     def test_content_type_no_charset(self):
81 81
         response = self.client.post('/content_type_no_charset/')
82 82
         self.assertEqual(get_charset(response), None)
83  
-    
  83
+
84 84
     def test_determine_charset(self):
85  
-        content_type, codec = determine_charset("", "utf-8;q=0.8,*;q=0.9")
  85
+        content_type, codec = get_response_encoding("", "utf-8;q=0.8,*;q=0.9")
86 86
         self.assertEqual(codec, get_codec("ISO-8859-1"))
87  
-        
  87
+
  88
+    def test_basic_response(self):
  89
+        "Make sure a normal request gets the default charset, with a 200 response."
  90
+        response = self.client.post('/basic_response/')
  91
+        self.assertEqual(response.status_code, 200)
  92
+        self.assertEqual(get_charset(response), settings.DEFAULT_CHARSET)
  93
+
1  tests/regressiontests/charsets/urls.py
@@ -19,4 +19,5 @@
19 19
     (r'^good_content_type/', views.good_content_type),
20 20
     (r'^bad_content_type/', views.bad_content_type),
21 21
     (r'^content_type_no_charset/', views.content_type_no_charset),
  22
+    (r'^basic_response/', views.basic_response),
22 23
 )
5  tests/regressiontests/charsets/views.py
@@ -14,4 +14,7 @@ def content_type_no_charset(request):
14 14
     return HttpResponse("UTF-8", content_type="text/html")
15 15
 
16 16
 def encode_response(request):
17  
-    return HttpResponse(u"\ue863", content_type="text/html; charset=GBK")
  17
+    return HttpResponse(u"\ue863", content_type="text/html; charset=GBK")
  18
+
  19
+def basic_response(request):
  20
+    return HttpResponse("ASCII.")

0 notes on commit 8d979ae

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