Browse files

Centralize formatting of HTTP-style dates.

Use time.strftime, which turns out to be a bit faster than either
datetime.strftime or email.utils.formatdate.
  • Loading branch information...
1 parent e5f4763 commit 2baf3c02ab3766ab623ef009a26b02f8e08ecb0b @bdarnell bdarnell committed Jan 27, 2013
Showing with 58 additions and 15 deletions.
  1. +2 −5 tornado/httpclient.py
  2. +23 −0 tornado/httputil.py
  3. +30 −1 tornado/test/httputil_test.py
  4. +3 −9 tornado/web.py
View
7 tornado/httpclient.py
@@ -31,8 +31,6 @@
from __future__ import absolute_import, division, print_function, with_statement
-import calendar
-import email.utils
import time
import weakref
@@ -279,9 +277,8 @@ def __init__(self, url, method="GET", headers=None, body=None,
if headers is None:
headers = httputil.HTTPHeaders()
if if_modified_since:
- timestamp = calendar.timegm(if_modified_since.utctimetuple())
- headers["If-Modified-Since"] = email.utils.formatdate(
- timestamp, localtime=False, usegmt=True)
+ headers["If-Modified-Since"] = httputil.format_timestamp(
+ if_modified_since)
self.proxy_host = proxy_host
self.proxy_port = proxy_port
self.proxy_username = proxy_username
View
23 tornado/httputil.py
@@ -18,7 +18,10 @@
from __future__ import absolute_import, division, print_function, with_statement
+import datetime
+import numbers
import re
+import time
from tornado.escape import native_str, parse_qs_bytes, utf8
from tornado.log import gen_log
@@ -288,6 +291,26 @@ def parse_multipart_form_data(boundary, data, arguments, files):
arguments.setdefault(name, []).append(value)
+def format_timestamp(ts):
+ """Formats a timestamp in the format used by HTTP.
+
+ The argument may be a numeric timestamp as returned by `time.time()`,
+ a time tuple as returned by `time.gmtime()`, or a `datetime.datetime`
+ object.
+
+ >>> format_timestamp(1359312200)
+ 'Sun, 27 Jan 2013 18:43:20 GMT'
+ """
+ if isinstance(ts, (tuple, time.struct_time)):
+ pass
+ elif isinstance(ts, datetime.datetime):
+ ts = ts.utctimetuple()
+ elif isinstance(ts, numbers.Real):
+ ts = time.gmtime(ts)
+ else:
+ raise TypeError("unknown timestamp type: %r" % ts)
+ return time.strftime("%a, %d %b %Y %H:%M:%S GMT", ts)
+
# _parseparam and _parse_header are copied and modified from python2.7's cgi.py
# The original 2.7 version of this code did not correctly support some
# combinations of semicolons and double quotes.
View
31 tornado/test/httputil_test.py
@@ -2,12 +2,15 @@
from __future__ import absolute_import, division, print_function, with_statement
-from tornado.httputil import url_concat, parse_multipart_form_data, HTTPHeaders
+from tornado.httputil import url_concat, parse_multipart_form_data, HTTPHeaders, format_timestamp
from tornado.escape import utf8
from tornado.log import gen_log
from tornado.testing import ExpectLog
from tornado.test.util import unittest
+
+import datetime
import logging
+import time
class TestUrlConcat(unittest.TestCase):
@@ -224,3 +227,29 @@ def test_multi_line(self):
[("Asdf", "qwer zxcv"),
("Foo", "bar baz"),
("Foo", "even more lines")])
+
+
+class FormatTimestampTest(unittest.TestCase):
+ # Make sure that all the input types are supported.
+ TIMESTAMP = 1359312200.503611
+ EXPECTED = 'Sun, 27 Jan 2013 18:43:20 GMT'
+
+ def check(self, value):
+ self.assertEqual(format_timestamp(value), self.EXPECTED)
+
+ def test_unix_time_float(self):
+ self.check(self.TIMESTAMP)
+
+ def test_unix_time_int(self):
+ self.check(int(self.TIMESTAMP))
+
+ def test_struct_time(self):
+ self.check(time.gmtime(self.TIMESTAMP))
+
+ def test_time_tuple(self):
+ tup = tuple(time.gmtime(self.TIMESTAMP))
+ self.assertEqual(9, len(tup))
+ self.check(tup)
+
+ def test_datetime(self):
+ self.check(datetime.datetime.utcfromtimestamp(self.TIMESTAMP))
View
12 tornado/web.py
@@ -53,14 +53,12 @@ def get(self):
import base64
import binascii
-import calendar
import datetime
import email.utils
import functools
import gzip
import hashlib
import hmac
-import itertools
import mimetypes
import numbers
import os.path
@@ -231,8 +229,7 @@ def clear(self):
self._headers = httputil.HTTPHeaders({
"Server": "TornadoServer/%s" % tornado.version,
"Content-Type": "text/html; charset=UTF-8",
- "Date": datetime.datetime.utcnow().strftime(
- "%a, %d %b %Y %H:%M:%S GMT"),
+ "Date": httputil.format_timestamp(time.gmtime()),
})
self.set_default_headers()
if not self.request.supports_http_1_1():
@@ -308,8 +305,7 @@ def _convert_header_value(self, value):
# return immediately since we know the converted value will be safe
return str(value)
elif isinstance(value, datetime.datetime):
- t = calendar.timegm(value.utctimetuple())
- return email.utils.formatdate(t, localtime=False, usegmt=True)
+ return httputil.format_timestamp(value)
else:
raise TypeError("Unsupported header value %r" % value)
# If \n is allowed into the header, it is possible to inject
@@ -410,9 +406,7 @@ def set_cookie(self, name, value, domain=None, expires=None, path="/",
expires = datetime.datetime.utcnow() + datetime.timedelta(
days=expires_days)
if expires:
- timestamp = calendar.timegm(expires.utctimetuple())
- morsel["expires"] = email.utils.formatdate(
- timestamp, localtime=False, usegmt=True)
+ morsel["expires"] = httputil.format_timestamp(expires)
if path:
morsel["path"] = path
for k, v in kwargs.items():

0 comments on commit 2baf3c0

Please sign in to comment.