Skip to content

Commit

Permalink
Centralize formatting of HTTP-style dates.
Browse files Browse the repository at this point in the history
Use time.strftime, which turns out to be a bit faster than either
datetime.strftime or email.utils.formatdate.
  • Loading branch information
bdarnell committed Jan 27, 2013
1 parent e5f4763 commit 2baf3c0
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 15 deletions.
7 changes: 2 additions & 5 deletions tornado/httpclient.py
Expand Up @@ -31,8 +31,6 @@

from __future__ import absolute_import, division, print_function, with_statement

import calendar
import email.utils
import time
import weakref

Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tornado/httputil.py
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
31 changes: 30 additions & 1 deletion tornado/test/httputil_test.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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))
12 changes: 3 additions & 9 deletions tornado/web.py
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down

0 comments on commit 2baf3c0

Please sign in to comment.