Skip to content

Commit c48c828

Browse files
greenrobotdan-obx
authored andcommitted
Plan B logic for broken timestamp close to epoch on Windows #9 #29
1 parent 5c0976a commit c48c828

File tree

3 files changed

+45
-4
lines changed

3 files changed

+45
-4
lines changed

objectbox/model/entity.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,22 @@ def set_object_id(self, object, id: int):
140140
@staticmethod
141141
def date_value_to_int(value, multiplier: int) -> int:
142142
if isinstance(value, datetime):
143-
return round(value.timestamp() * multiplier) # timestamp returns seconds
143+
try:
144+
return round(value.timestamp() * multiplier) # timestamp returns seconds
145+
except OSError:
146+
# On Windows, timestamp() raises an OSError for naive datetime objects with dates is close to the epoch.
147+
# Thus, it is highly recommended to only use datetime *with* timezone information (no issue here).
148+
# See bug reports:
149+
# https://github.com/python/cpython/issues/81708 and https://github.com/python/cpython/issues/94414
150+
# The workaround is to go via timezone-aware datetime objects, which seem to work - with one caveat.
151+
local_tz = datetime.now().astimezone().tzinfo
152+
value = value.replace(tzinfo=local_tz)
153+
value = value.astimezone(timezone.utc)
154+
# Caveat: times may be off by; offset should be 0 but actually was seen at -3600 in CEST (Linux & Win).
155+
# See also https://stackoverflow.com/q/56931738/551269
156+
# So, let's check value 0 as a reference and use the resulting timestamp as an offset for correction.
157+
offset = datetime.fromtimestamp(0).replace(tzinfo=local_tz).astimezone(timezone.utc).timestamp()
158+
return round((value.timestamp() - offset) * multiplier) # timestamp returns seconds
144159
elif isinstance(value, float):
145160
return round(value * multiplier) # floats typically represent seconds
146161
elif isinstance(value, int): # Interpret ints as-is (without the multiplier); e.g. milliseconds or nanoseconds

tests/test_box.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def test_datetime_special_values():
206206
assert read.date_nano == datetime.fromtimestamp(0, timezone.utc)
207207

208208
object.date = datetime.fromtimestamp(1.0, timezone.utc)
209-
object.date_nano = datetime.fromtimestamp(1.0)
209+
object.date_nano = datetime.fromtimestamp(1.0, timezone.utc)
210210
id = box.put(object)
211211

212212
read = box.get(id)

tests/test_utils.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,42 @@ def test_date_value_to_int__basics():
1212
assert _Entity.date_value_to_int(1234, 1000000000) == 1234
1313
assert _Entity.date_value_to_int(1234.0, 1000) == 1234000 # milliseconds
1414
assert _Entity.date_value_to_int(1234.0, 1000000000) == 1234000000000 # nanoseconds
15+
dt = datetime.fromtimestamp(12345678) # May 1970; 1234 is too close to the epoch (special case for that below)
16+
assert _Entity.date_value_to_int(dt, 1000) == 12345678000 # milliseconds
17+
18+
19+
def test_date_value_to_int__close_to_epoch():
20+
assert _Entity.date_value_to_int(datetime.fromtimestamp(0, timezone.utc), 1000) == 0
21+
assert _Entity.date_value_to_int(datetime.fromtimestamp(1234, timezone.utc), 1000) == 1234000
22+
assert _Entity.date_value_to_int(datetime.fromtimestamp(0), 1000) == 0
23+
assert _Entity.date_value_to_int(datetime.fromtimestamp(1234), 1000) == 1234000
24+
25+
# "Return the local date corresponding to the POSIX timestamp"; but not always!? Was -1 hour off with CEST:
26+
dt0naive = datetime.fromtimestamp(0)
27+
local_tz = datetime.now().astimezone().tzinfo
28+
dt0local = dt0naive.replace(tzinfo=local_tz)
29+
dt0utc = dt0local.astimezone(timezone.utc)
30+
31+
# Print, don't assert... the result Seems to depend on the local timezone configuration!?
32+
print("\nNaive:", dt0naive) # Seen: 1970-01-01 01:00:00
33+
print("Local:", dt0local) # Seen: 1970-01-01 01:00:00+02:00
34+
print("UTC:", dt0utc) # Seen: 1969-12-31 23:00:00+00:00
35+
print("Timestamp:", dt0utc.timestamp()) # Seen: -3600.0
36+
1537
dt = datetime.fromtimestamp(1234)
1638
if sys.platform == "win32":
39+
# On Windows, timestamp() seems to raise an OSError if the date is close to the epoch; see bug reports:
40+
# https://github.com/python/cpython/issues/81708 and https://github.com/python/cpython/issues/94414
1741
try:
1842
dt.timestamp()
19-
assert False, "Expected OSError"
43+
assert False, "Expected OSError - Did Python on Windows get fixed?"
2044
except OSError as e:
2145
assert e.errno == 22
2246
else:
47+
# Non-Windows platforms should work fine
2348
assert dt.timestamp() == 1234
24-
assert _Entity.date_value_to_int(dt, 1000) == 1234000 # milliseconds
49+
50+
assert _Entity.date_value_to_int(dt, 1000) == 1234000 # milliseconds
2551

2652

2753
def test_date_value_to_int__timezone():

0 commit comments

Comments
 (0)