From 829a692f9b6a5e4fa8c3184bdb52e08d179063db Mon Sep 17 00:00:00 2001 From: Chris Rebert Date: Sat, 20 Jul 2013 13:59:16 -0700 Subject: [PATCH] Fix #408: Response.age is semantically a timedelta, not a datetime --- CHANGES | 5 ++++- tests/test_wrappers.py | 11 +++++++++-- werkzeug/http.py | 43 ++++++++++++++++++++++++++++++++++++++++++ werkzeug/wrappers.py | 5 +++-- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 591df094a..0f68b74f2 100644 --- a/CHANGES +++ b/CHANGES @@ -7,8 +7,11 @@ Version 0.13 yet to be released - Raise `TypeError` when port is not an integer. -- Fully deprecate `werkzeug.script`. Use `click` +- Fully deprecate `werkzeug.script`. Use `click` (http://click.pocoo.org) instead. +- ``response.age`` is parsed as a ``timedelta``. Previously, it was incorrectly + treated as a ``datetime``. The header value is an integer number of seconds, + not a date string. (``#414``) Version 0.12.1 -------------- diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py index ac1d5d131..0e467295c 100644 --- a/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -14,7 +14,7 @@ import pickle from io import BytesIO -from datetime import datetime +from datetime import datetime, timedelta from werkzeug._compat import iteritems from tests import strict_eq @@ -696,11 +696,18 @@ def test_common_response_descriptors_mixin(): response.content_length = '42' assert response.content_length == 42 - for attr in 'date', 'age', 'expires': + for attr in 'date', 'expires': assert getattr(response, attr) is None setattr(response, attr, now) assert getattr(response, attr) == now + assert response.age is None + age_td = timedelta(days=1, minutes=3, seconds=5) + response.age = age_td + assert response.age == age_td + response.age = 42 + assert response.age == timedelta(seconds=42) + assert response.retry_after is None response.retry_after = now assert response.retry_after == now diff --git a/werkzeug/http.py b/werkzeug/http.py index 0b1876f2f..6d6ea6873 100644 --- a/werkzeug/http.py +++ b/werkzeug/http.py @@ -781,6 +781,49 @@ def http_date(timestamp=None): return _dump_date(timestamp, ' ') +def parse_age(value=None): + """Parses a base-10 integer count of seconds into a timedelta. + + If parsing fails, the return value is `None`. + + :param value: a string consisting of an integer represented in base-10 + :return: a :class:`datetime.timedelta` object or `None`. + """ + if not value: + return None + try: + seconds = int(value) + except ValueError: + return None + if seconds < 0: + return None + try: + return timedelta(seconds=seconds) + except OverflowError: + return None + + +def dump_age(age=None): + """Formats the duration as a base-10 integer. + + :param age: should be an integer number of seconds, + a :class:`datetime.timedelta` object, or, + if the age is unknown, `None` (default). + """ + if age is None: + return + if isinstance(age, timedelta): + # do the equivalent of Python 2.7's timedelta.total_seconds(), + # but disregarding fractional seconds + age = age.seconds + (age.days * 24 * 3600) + + age = int(age) + if age < 0: + raise ValueError('age cannot be negative') + + return str(age) + + def is_resource_modified(environ, etag=None, data=None, last_modified=None, ignore_if_range=True): """Convenience method for conditional requests. diff --git a/werkzeug/wrappers.py b/werkzeug/wrappers.py index 437339d27..49cebd69b 100644 --- a/werkzeug/wrappers.py +++ b/werkzeug/wrappers.py @@ -31,7 +31,8 @@ parse_www_authenticate_header, remove_entity_headers, \ parse_options_header, dump_options_header, http_date, \ parse_if_range_header, parse_cookie, dump_cookie, \ - parse_range_header, parse_content_range_header, dump_header + parse_range_header, parse_content_range_header, dump_header, \ + parse_age, dump_age from werkzeug.urls import url_decode, iri_to_uri, url_join from werkzeug.formparser import FormDataParser, default_stream_factory from werkzeug.utils import cached_property, environ_property, \ @@ -1824,7 +1825,7 @@ def on_update(d): The Location response-header field is used to redirect the recipient to a location other than the Request-URI for completion of the request or identification of a new resource.''') - age = header_property('Age', None, parse_date, http_date, doc=''' + age = header_property('Age', None, parse_age, dump_age, doc=''' The Age response-header field conveys the sender's estimate of the amount of time since the response (or its revalidation) was generated at the origin server.