Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Continue #414: Fix #408: Response.age is semantically a timedelta, not a datetime #1104

Merged
merged 1 commit into from
Apr 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------
Expand Down
11 changes: 9 additions & 2 deletions tests/test_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions werkzeug/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions werkzeug/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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.
Expand Down