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

feat(misc): return timezone aware datetime objects #2246

Merged
merged 13 commits into from
Aug 27, 2024
Merged
2 changes: 2 additions & 0 deletions docs/_newsfragments/2182.breakingchange.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The function :func:`falcon.http_date_to_dt` now validates http-dates to have the correct
timezone set. It now also returns timezone aware datetime objects.
17 changes: 15 additions & 2 deletions falcon/util/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@

_UNSAFE_CHARS = re.compile(r'[^a-zA-Z0-9.-]')

_ALLOWED_HTTP_TIMEZONES: Tuple[str, ...] = ('GMT', 'UTC')
vytas7 marked this conversation as resolved.
Show resolved Hide resolved

_UTC_TIMEZONE = datetime.timezone.utc

# PERF(kgriffs): Avoid superfluous namespace lookups
_strptime: Callable[[str, str], datetime.datetime] = datetime.datetime.strptime
_utcnow: Callable[[], datetime.datetime] = functools.partial(
Expand Down Expand Up @@ -176,15 +180,24 @@

Raises:
ValueError: http_date doesn't match any of the available time formats
ValueError: http_date doesn't match allowed timezones
"""
date_timezone_str = http_date[-3:]
has_tz_identifier = not date_timezone_str.isdigit()

Check warning on line 186 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L185-L186

Added lines #L185 - L186 were not covered by tests
if date_timezone_str not in _ALLOWED_HTTP_TIMEZONES and has_tz_identifier:
CaselIT marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(

Check warning on line 188 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L188

Added line #L188 was not covered by tests
'timezone information of time data %r is not allowed' % http_date
)

if not obs_date:
# PERF(kgriffs): This violates DRY, but we do it anyway
# to avoid the overhead of setting up a tuple, looping
# over it, and setting up exception handling blocks each
# time around the loop, in the case that we don't actually
# need to check for multiple formats.
return _strptime(http_date, '%a, %d %b %Y %H:%M:%S %Z')
return _strptime(http_date, '%a, %d %b %Y %H:%M:%S %Z').replace(

Check warning on line 198 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L198

Added line #L198 was not covered by tests
tzinfo=_UTC_TIMEZONE
)

time_formats = (
'%a, %d %b %Y %H:%M:%S %Z',
Expand All @@ -196,7 +209,7 @@
# Loop through the formats and return the first that matches
for time_format in time_formats:
try:
return _strptime(http_date, time_format)
return _strptime(http_date, time_format).replace(tzinfo=_UTC_TIMEZONE)

Check warning on line 212 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L212

Added line #L212 was not covered by tests
vytas7 marked this conversation as resolved.
Show resolved Hide resolved
except ValueError:
continue

Expand Down
21 changes: 11 additions & 10 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import falcon.testing as testing
from falcon.util import http_date_to_dt
from falcon.util import TimezoneGMT
from falcon.util.misc import _utcnow

from _util import create_app # NOQA

UNICODE_TEST_STRING = 'Unicode_\xc3\xa6\xc3\xb8'

Expand All @@ -30,10 +33,6 @@ def dst(self, dt):
GMT_PLUS_ONE = TimezoneGMTPlus1()


def utcnow_naive():
return datetime.now(timezone.utc).replace(tzinfo=None)


class CookieResource:
def on_get(self, req, resp):
resp.set_cookie('foo', 'bar', domain='example.com', path='/')
Expand Down Expand Up @@ -187,7 +186,7 @@ def test_response_complex_case(client):
assert cookie.domain is None
assert cookie.same_site == 'Lax'

assert cookie.expires < utcnow_naive()
assert cookie.expires < _utcnow()

# NOTE(kgriffs): I know accessing a private attr like this is
# naughty of me, but we just need to sanity-check that the
Expand All @@ -209,7 +208,7 @@ def test(cookie, path, domain, samesite='Lax'):
assert cookie.domain == domain
assert cookie.path == path
assert cookie.same_site == samesite
assert cookie.expires < utcnow_naive()
assert cookie.expires < _utcnow()

test(result.cookies['foo'], path=None, domain=None)
test(result.cookies['bar'], path='/bar', domain=None)
Expand Down Expand Up @@ -247,7 +246,7 @@ def test_set(cookie, value, samesite=None):
def test_unset(cookie, samesite='Lax'):
assert cookie.value == '' # An unset cookie has an empty value
assert cookie.same_site == samesite
assert cookie.expires < utcnow_naive()
assert cookie.expires < _utcnow()

test_unset(result_unset.cookies['foo'], samesite='Strict')
# default: bar is unset with no samesite param, so should go to Lax
Expand All @@ -270,7 +269,7 @@ def test_cookie_expires_naive(client):
cookie = result.cookies['foo']
assert cookie.value == 'bar'
assert cookie.domain is None
assert cookie.expires == datetime(year=2050, month=1, day=1)
assert cookie.expires == datetime(year=2050, month=1, day=1, tzinfo=timezone.utc)
assert not cookie.http_only
assert cookie.max_age is None
assert cookie.path is None
Expand All @@ -283,7 +282,9 @@ def test_cookie_expires_aware(client):
cookie = result.cookies['foo']
assert cookie.value == 'bar'
assert cookie.domain is None
assert cookie.expires == datetime(year=2049, month=12, day=31, hour=23)
assert cookie.expires == datetime(
year=2049, month=12, day=31, hour=23, tzinfo=timezone.utc
)
assert not cookie.http_only
assert cookie.max_age is None
assert cookie.path is None
Expand Down Expand Up @@ -341,7 +342,7 @@ def test_response_unset_cookie(client):
assert match

expiration = http_date_to_dt(match.group(1), obs_date=True)
assert expiration < utcnow_naive()
assert expiration < _utcnow()


def test_cookie_timezone(client):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_request_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ def test_bogus_content_length_neg(self, asgi):
],
)
def test_date(self, asgi, header, attr):
date = datetime.datetime(2013, 4, 4, 5, 19, 18)
date = datetime.datetime(2013, 4, 4, 5, 19, 18, tzinfo=datetime.timezone.utc)
date_str = 'Thu, 04 Apr 2013 05:19:18 GMT'

headers = {header: date_str}
Expand Down
18 changes: 11 additions & 7 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,29 +126,30 @@ def test_http_now(self):

def test_dt_to_http(self):
assert (
falcon.dt_to_http(datetime(2013, 4, 4)) == 'Thu, 04 Apr 2013 00:00:00 GMT'
falcon.dt_to_http(datetime(2013, 4, 4, tzinfo=timezone.utc))
== 'Thu, 04 Apr 2013 00:00:00 GMT'
)

assert (
falcon.dt_to_http(datetime(2013, 4, 4, 10, 28, 54))
falcon.dt_to_http(datetime(2013, 4, 4, 10, 28, 54, tzinfo=timezone.utc))
== 'Thu, 04 Apr 2013 10:28:54 GMT'
)

def test_http_date_to_dt(self):
assert falcon.http_date_to_dt('Thu, 04 Apr 2013 00:00:00 GMT') == datetime(
2013, 4, 4
2013, 4, 4, tzinfo=timezone.utc
)

assert falcon.http_date_to_dt('Thu, 04 Apr 2013 10:28:54 GMT') == datetime(
2013, 4, 4, 10, 28, 54
2013, 4, 4, 10, 28, 54, tzinfo=timezone.utc
)

with pytest.raises(ValueError):
falcon.http_date_to_dt('Thu, 04-Apr-2013 10:28:54 GMT')

assert falcon.http_date_to_dt(
'Thu, 04-Apr-2013 10:28:54 GMT', obs_date=True
) == datetime(2013, 4, 4, 10, 28, 54)
) == datetime(2013, 4, 4, 10, 28, 54, tzinfo=timezone.utc)

with pytest.raises(ValueError):
falcon.http_date_to_dt('Sun Nov 6 08:49:37 1994')
Expand All @@ -158,11 +159,14 @@ def test_http_date_to_dt(self):

assert falcon.http_date_to_dt(
'Sun Nov 6 08:49:37 1994', obs_date=True
) == datetime(1994, 11, 6, 8, 49, 37)
) == datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone.utc)

assert falcon.http_date_to_dt(
'Sunday, 06-Nov-94 08:49:37 GMT', obs_date=True
) == datetime(1994, 11, 6, 8, 49, 37)
) == datetime(1994, 11, 6, 8, 49, 37, tzinfo=timezone.utc)

with pytest.raises(ValueError):
falcon.http_date_to_dt('Thu, 04 Apr 2013 10:28:54 EST')

def test_pack_query_params_none(self):
assert falcon.to_query_str({}) == ''
Expand Down
Loading