Skip to content

Commit a5efddf

Browse files
KeyBuilder: add datetime hashing (#219)
* KeyBuilder: add datetime hashing * (try to) take naive/aware, timezones into account * add another test * fix datetime timezones * another time fix
1 parent c55eba1 commit a5efddf

2 files changed

Lines changed: 134 additions & 4 deletions

File tree

pytools/persistent_dict.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,19 +402,19 @@ def update_for_NoneType(key_hash: Hash, key: None) -> None: # noqa: N802
402402
key_hash.update(b"<None>")
403403

404404
@staticmethod
405-
def update_for_dtype(key_hash, key):
405+
def update_for_dtype(key_hash: Hash, key: Any) -> None:
406406
key_hash.update(key.str.encode("utf8"))
407407

408408
# Handling numpy >= 1.20, for which
409409
# type(np.dtype("float32")) -> "dtype[float32]"
410410
# Introducing this method allows subclasses to specially handle all those
411411
# dtypes.
412412
@staticmethod
413-
def update_for_specific_dtype(key_hash, key):
413+
def update_for_specific_dtype(key_hash: Hash, key: Any) -> None:
414414
key_hash.update(key.str.encode("utf8"))
415415

416416
@staticmethod
417-
def update_for_numpy_scalar(key_hash: Hash, key) -> None:
417+
def update_for_numpy_scalar(key_hash: Hash, key: Any) -> None:
418418
import numpy as np
419419
if hasattr(np, "complex256") and key.dtype == np.dtype("complex256"):
420420
key_hash.update(repr(complex(key)).encode("utf8"))
@@ -430,7 +430,7 @@ def update_for_dataclass(self, key_hash: Hash, key: Any) -> None:
430430
self.rec(key_hash, fld.name)
431431
self.rec(key_hash, getattr(key, fld.name, None))
432432

433-
def update_for_attrs(self, key_hash: Hash, key) -> None:
433+
def update_for_attrs(self, key_hash: Hash, key: Any) -> None:
434434
self.rec(key_hash, f"{type(key).__qualname__}.{type(key).__name__}")
435435

436436
for fld in attrs.fields(key.__class__):
@@ -449,6 +449,37 @@ def update_for_frozendict(self, key_hash: Hash, key: Mapping) -> None:
449449
update_for_PMap = update_for_frozendict # noqa: N815
450450
update_for_Map = update_for_frozendict # noqa: N815
451451

452+
# {{{ date, time, datetime, timezone
453+
454+
def update_for_date(self, key_hash: Hash, key: Any) -> None:
455+
# 'date' has no timezone information; it is always naive
456+
self.rec(key_hash, key.isoformat())
457+
458+
def update_for_time(self, key_hash: Hash, key: Any) -> None:
459+
# 'time' should differentiate between naive and aware
460+
import datetime
461+
462+
# Convert to datetime object
463+
self.rec(key_hash, datetime.datetime.combine(datetime.date.today(), key))
464+
self.rec(key_hash, "<time>")
465+
466+
def update_for_datetime(self, key_hash: Hash, key: Any) -> None:
467+
# 'datetime' should differentiate between naive and aware
468+
469+
# https://docs.python.org/3.11/library/datetime.html#determining-if-an-object-is-aware-or-naive
470+
if key.tzinfo is not None and key.tzinfo.utcoffset(key) is not None:
471+
self.rec(key_hash, key.timestamp())
472+
self.rec(key_hash, "<aware>")
473+
else:
474+
from datetime import timezone
475+
self.rec(key_hash, key.replace(tzinfo=timezone.utc).timestamp())
476+
self.rec(key_hash, "<naive>")
477+
478+
def update_for_timezone(self, key_hash: Hash, key: Any) -> None:
479+
self.rec(key_hash, repr(key))
480+
481+
# }}}
482+
452483
# }}}
453484

454485
# }}}

pytools/test/test_persistent_dict.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,105 @@ class MyAttrs2:
598598
!= keyb(MyAttrs("hi", 1))) # type: ignore[call-arg]
599599

600600

601+
def test_datetime_hashing() -> None:
602+
keyb = KeyBuilder()
603+
604+
import datetime
605+
606+
# {{{ date
607+
# No timezone info; date is always naive
608+
assert (keyb(datetime.date(2020, 1, 1))
609+
== keyb(datetime.date(2020, 1, 1))
610+
== "9fb97d7faabc3603f3e334ca5eb1eb0fe0c92665e5611cb1b5aa77fa0f70f5e3")
611+
assert keyb(datetime.date(2020, 1, 1)) != keyb(datetime.date(2020, 1, 2))
612+
613+
# }}}
614+
615+
# {{{ time
616+
617+
# Must distinguish between naive and aware time objects
618+
619+
# Naive time
620+
assert (keyb(datetime.time(12, 0))
621+
== keyb(datetime.time(12, 0))
622+
== keyb(datetime.time(12, 0, 0))
623+
== keyb(datetime.time(12, 0, 0, 0))
624+
== "bf73f48b2f2666b5c42f6993e628fdc15e0b6c3127186c3ab44ce08ed83d0472")
625+
assert keyb(datetime.time(12, 0)) != keyb(datetime.time(12, 1))
626+
627+
# Aware time
628+
t1 = datetime.time(12, 0, tzinfo=datetime.timezone.utc)
629+
t2 = datetime.time(7, 0,
630+
tzinfo=datetime.timezone(datetime.timedelta(hours=-5)))
631+
t3 = datetime.time(7, 0,
632+
tzinfo=datetime.timezone(datetime.timedelta(hours=-4)))
633+
634+
assert t1 == t2
635+
assert (keyb(t1)
636+
== keyb(t2)
637+
== "c0947587c92ab6e2df90475dd497aff1d83df55fbd5af6c55b2a0a221b2437a4")
638+
639+
assert t1 != t3
640+
assert keyb(t1) != keyb(t3)
641+
642+
# }}}
643+
644+
# {{{ datetime
645+
646+
# must distinguish between naive and aware datetime objects
647+
648+
# Aware datetime
649+
dt1 = datetime.datetime(2020, 1, 1, 12, tzinfo=datetime.timezone.utc)
650+
dt2 = datetime.datetime(2020, 1, 1, 7,
651+
tzinfo=datetime.timezone(datetime.timedelta(hours=-5)))
652+
653+
assert dt1 == dt2
654+
assert (keyb(dt1)
655+
== keyb(dt2)
656+
== "cd35722af47e42cb3bc81c389b87eb2e78ee8e20298bb1d8a193b30940d1c142")
657+
658+
dt3 = datetime.datetime(2020, 1, 1, 7,
659+
tzinfo=datetime.timezone(datetime.timedelta(hours=-4)))
660+
661+
assert dt1 != dt3
662+
assert keyb(dt1) != keyb(dt3)
663+
664+
# Naive datetime
665+
dt4 = datetime.datetime(2020, 1, 1, 6) # matches dt1 'naively'
666+
assert dt1 != dt4 # naive and aware datetime objects are never equal
667+
assert keyb(dt1) != keyb(dt4)
668+
669+
assert (keyb(datetime.datetime(2020, 1, 1))
670+
== keyb(datetime.datetime(2020, 1, 1))
671+
== keyb(datetime.datetime(2020, 1, 1, 0, 0, 0, 0))
672+
== "8f3b843d7b9176afd8e2ce97ebc19789098a1c7774c4ec00d4054ec954ce2b88"
673+
)
674+
assert keyb(datetime.datetime(2020, 1, 1)) != keyb(datetime.datetime(2020, 1, 2))
675+
assert (keyb(datetime.datetime(2020, 1, 1))
676+
!= keyb(datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc)))
677+
678+
# }}}
679+
680+
# {{{ timezone
681+
682+
tz1 = datetime.timezone(datetime.timedelta(hours=-4))
683+
tz2 = datetime.timezone(datetime.timedelta(hours=0))
684+
tz3 = datetime.timezone.utc
685+
686+
assert tz1 != tz2
687+
assert keyb(tz1) != keyb(tz2)
688+
689+
assert tz1 != tz3
690+
assert keyb(tz1) != keyb(tz3)
691+
692+
assert tz2 == tz3
693+
assert (keyb(tz2)
694+
== keyb(tz3)
695+
== "89bd615f32c1f209b0853b1fc7d06ddb6fda7f367a00a8621d60337d52cb8d10")
696+
697+
# }}}
698+
699+
601700
def test_xdg_cache_home() -> None:
602701
import os
603702
xdg_dir = "tmpdir_pytools_xdg_test"

0 commit comments

Comments
 (0)