Skip to content

Commit 3b1a498

Browse files
author
joekohlsdorf
committed
Fix timezone handling for datetime to unixtime conversions
datetime objects are supported to set expire, these can have timezones. mktime was used to convert these to unixtime. mktime in Python however is not timezone aware, it expects the input to be UTC and redis-py did not convert the datetime timestamps to UTC before calling mktime. This can lead to: 1) Setting incorrect expire times because the input datetime object has a timezone but is passed to mktime without converting to UTC first. 2) When the datetime timestamp is within DST, mktime fails with "OverflowError: mktime argument out of range" because UTC doesn't have DST. This depends on libc versions.
1 parent bedf3c8 commit 3b1a498

File tree

4 files changed

+13
-20
lines changed

4 files changed

+13
-20
lines changed

CHANGES

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
* Fix timezone handling for datetime to unixtime conversions
22
* Allow negative `retries` for `Retry` class to retry forever
33
* Add `items` parameter to `hset` signature
44
* Create codeql-analysis.yml (#1988). Thanks @chayim

redis/commands/core.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import datetime
44
import hashlib
5-
import time
65
import warnings
76
from typing import (
87
TYPE_CHECKING,
@@ -1667,7 +1666,7 @@ def expireat(
16671666
For more information see https://redis.io/commands/expireat
16681667
"""
16691668
if isinstance(when, datetime.datetime):
1670-
when = int(time.mktime(when.timetuple()))
1669+
when = int(when.timestamp())
16711670

16721671
exp_option = list()
16731672
if nx:
@@ -1762,14 +1761,12 @@ def getex(
17621761
if exat is not None:
17631762
pieces.append("EXAT")
17641763
if isinstance(exat, datetime.datetime):
1765-
s = int(exat.microsecond / 1000000)
1766-
exat = int(time.mktime(exat.timetuple())) + s
1764+
exat = int(exat.timestamp())
17671765
pieces.append(exat)
17681766
if pxat is not None:
17691767
pieces.append("PXAT")
17701768
if isinstance(pxat, datetime.datetime):
1771-
ms = int(pxat.microsecond / 1000)
1772-
pxat = int(time.mktime(pxat.timetuple())) * 1000 + ms
1769+
pxat = int(pxat.timestamp() * 1000)
17731770
pieces.append(pxat)
17741771
if persist:
17751772
pieces.append("PERSIST")
@@ -1988,8 +1985,7 @@ def pexpireat(
19881985
For more information see https://redis.io/commands/pexpireat
19891986
"""
19901987
if isinstance(when, datetime.datetime):
1991-
ms = int(when.microsecond / 1000)
1992-
when = int(time.mktime(when.timetuple())) * 1000 + ms
1988+
when = int(when.timestamp() * 1000)
19931989
exp_option = list()
19941990
if nx:
19951991
exp_option.append("NX")
@@ -2190,14 +2186,12 @@ def set(
21902186
if exat is not None:
21912187
pieces.append("EXAT")
21922188
if isinstance(exat, datetime.datetime):
2193-
s = int(exat.microsecond / 1000000)
2194-
exat = int(time.mktime(exat.timetuple())) + s
2189+
exat = int(exat.timestamp())
21952190
pieces.append(exat)
21962191
if pxat is not None:
21972192
pieces.append("PXAT")
21982193
if isinstance(pxat, datetime.datetime):
2199-
ms = int(pxat.microsecond / 1000)
2200-
pxat = int(time.mktime(pxat.timetuple())) * 1000 + ms
2194+
pxat = int(pxat.timestamp() * 1000)
22012195
pieces.append(pxat)
22022196
if keepttl:
22032197
pieces.append("KEEPTTL")

tests/test_asyncio/test_commands.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import datetime
66
import re
77
import sys
8-
import time
98
from string import ascii_letters
109

1110
import pytest
@@ -805,7 +804,7 @@ async def test_expireat_no_key(self, r: redis.Redis):
805804
async def test_expireat_unixtime(self, r: redis.Redis):
806805
expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
807806
await r.set("a", "foo")
808-
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
807+
expire_at_seconds = int(expire_at.timestamp())
809808
assert await r.expireat("a", expire_at_seconds)
810809
assert 0 < await r.ttl("a") <= 61
811810

@@ -930,8 +929,8 @@ async def test_pexpireat_no_key(self, r: redis.Redis):
930929
async def test_pexpireat_unixtime(self, r: redis.Redis):
931930
expire_at = await redis_server_time(r) + datetime.timedelta(minutes=1)
932931
await r.set("a", "foo")
933-
expire_at_seconds = int(time.mktime(expire_at.timetuple())) * 1000
934-
assert await r.pexpireat("a", expire_at_seconds)
932+
expire_at_milliseconds = int(expire_at.timestamp() * 1000)
933+
assert await r.pexpireat("a", expire_at_milliseconds)
935934
assert 0 < await r.pttl("a") <= 61000
936935

937936
@skip_if_server_version_lt("2.6.0")

tests/test_commands.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ def test_expireat_no_key(self, r):
11851185
def test_expireat_unixtime(self, r):
11861186
expire_at = redis_server_time(r) + datetime.timedelta(minutes=1)
11871187
r["a"] = "foo"
1188-
expire_at_seconds = int(time.mktime(expire_at.timetuple()))
1188+
expire_at_seconds = int(expire_at.timestamp())
11891189
assert r.expireat("a", expire_at_seconds) is True
11901190
assert 0 < r.ttl("a") <= 61
11911191

@@ -1428,8 +1428,8 @@ def test_pexpireat_no_key(self, r):
14281428
def test_pexpireat_unixtime(self, r):
14291429
expire_at = redis_server_time(r) + datetime.timedelta(minutes=1)
14301430
r["a"] = "foo"
1431-
expire_at_seconds = int(time.mktime(expire_at.timetuple())) * 1000
1432-
assert r.pexpireat("a", expire_at_seconds) is True
1431+
expire_at_milliseconds = int(expire_at.timestamp() * 1000)
1432+
assert r.pexpireat("a", expire_at_milliseconds) is True
14331433
assert 0 < r.pttl("a") <= 61000
14341434

14351435
@skip_if_server_version_lt("7.0.0")

0 commit comments

Comments
 (0)