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 @@ -74,6 +74,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 @@ -181,15 +185,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 191 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L190-L191

Added lines #L190 - L191 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 193 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L193

Added line #L193 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 203 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L203

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

time_formats = (
'%a, %d %b %Y %H:%M:%S %Z',
Expand All @@ -201,7 +214,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 217 in falcon/util/misc.py

View check run for this annotation

Codecov / codecov/patch

falcon/util/misc.py#L217

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

Expand Down
19 changes: 4 additions & 15 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def dst(self, dt):


def utcnow_naive():
chgad marked this conversation as resolved.
Show resolved Hide resolved
return datetime.now(timezone.utc).replace(tzinfo=None)
return datetime.now(timezone.utc)


class CookieResource:
Expand Down Expand Up @@ -252,26 +252,15 @@ def test_unset(cookie, samesite='Lax'):
assert not result_unset.cookies['baz'].same_site


def test_cookie_expires_naive(client):
CaselIT marked this conversation as resolved.
Show resolved Hide resolved
result = client.simulate_post('/')

cookie = result.cookies['foo']
assert cookie.value == 'bar'
assert cookie.domain is None
assert cookie.expires == datetime(year=2050, month=1, day=1)
assert not cookie.http_only
assert cookie.max_age is None
assert cookie.path is None
assert not cookie.secure


def test_cookie_expires_aware(client):
result = client.simulate_put('/')

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
2 changes: 1 addition & 1 deletion tests/test_request_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,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 @@ -118,29 +118,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 @@ -150,11 +151,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