From 2baf3c02ab3766ab623ef009a26b02f8e08ecb0b Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sun, 27 Jan 2013 14:01:59 -0500 Subject: [PATCH] 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. --- tornado/httpclient.py | 7 ++----- tornado/httputil.py | 23 +++++++++++++++++++++++ tornado/test/httputil_test.py | 31 ++++++++++++++++++++++++++++++- tornado/web.py | 12 +++--------- 4 files changed, 58 insertions(+), 15 deletions(-) diff --git a/tornado/httpclient.py b/tornado/httpclient.py index 6fecdebb..73a88af1 100644 --- a/tornado/httpclient.py +++ b/tornado/httpclient.py @@ -31,8 +31,6 @@ supported version is 7.18.2, and the recommended version is 7.21.1 or newer. from __future__ import absolute_import, division, print_function, with_statement -import calendar -import email.utils import time import weakref @@ -279,9 +277,8 @@ class HTTPRequest(object): 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 diff --git a/tornado/httputil.py b/tornado/httputil.py index cd0aec7f..163e3035 100644 --- a/tornado/httputil.py +++ b/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. diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py index 026da5ee..1e84da76 100644 --- a/tornado/test/httputil_test.py +++ b/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 @@ Foo: even [("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)) diff --git a/tornado/web.py b/tornado/web.py index 306c85ea..35f4e098 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -53,14 +53,12 @@ from __future__ import absolute_import, division, print_function, with_statement 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 @@ class RequestHandler(object): 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 @@ class RequestHandler(object): # 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 @@ class RequestHandler(object): 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():