[PEAK] Trellis and datetime.datetime

Jeffrey Harris jeffrey at osafoundation.org
Fri Jan 2 17:58:51 EST 2009


Happy New Year PEAKfolk!

In playing with Trellis and datetimes, I've come across an issue.  One
could argue my issue is actually with datetime.datetime's __eq__ method,
but I'd rather not have Yet Another DateTime Implementation, so...

Basically, the problem is that two datetimes which represent the same
instant in time compare as equal, even if their timezones are different.

This presents a problem if you actually care about a datetime's timezone
and you're using the Trellis.  Rules that depend on datetimes won't
always get updated if a timezone is changed.

One way to work around this would be to just never have datetime valued
Trellis cells, and instead store two cells, a timestamp and a timezone.


Alternately, making Trellis datetime aware isn't hard (sample patch
below).  If there was an entry_point to plug-in additional equality
tests for determining whether or not to propagate value changes, it
wouldn't need to be so datetime specific...

Here's a sample doctest to demonstrate the problem, and the patch.

>>> from peak.events import trellis
>>> from datetime import timedelta, datetime, tzinfo
>>> class FixedOffset(tzinfo):
...    def __init__(self, offset, name):
...        self.__offset = timedelta(minutes = offset)
...        self.__name = name
...
...    def __repr__(self):
...        return "<%s>" % self.__name
...
...    def utcoffset(self, dt):
...        return self.__offset
...
...    def tzname(self, dt):
...        return self.__name
...
...    def dst(self, dt):
...        return timedelta(0)

>>> pacific = FixedOffset(-480, "US/Pacific")
>>> eastern = FixedOffset(-300, "US/Eastern")

Test with datetime attribute dt, and tzinfo rule computed from dt:

>>> class Dated(trellis.Component):
...     dt = trellis.attr(None)
...
...     @trellis.compute
...     def later(self):
...         return self.dt + timedelta(hours=1)
...
...     @trellis.compute
...     def tzinfo(self):
...         return self.dt.tzinfo
...
...     @trellis.maintain
...     def compound(self):
...         return "OK: %s" % self.tzinfo

>>> d = Dated(dt=datetime(2008,12,30,3, tzinfo=pacific))
>>> d.tzinfo
<US/Pacific>
>>> d.compound
'OK: <US/Pacific>'
>>> d.later
datetime.datetime(2008, 12, 30, 4, 0, tzinfo=<US/Pacific>)
>>> d.dt = d.dt.astimezone(eastern)
>>> d.compound # fails
'OK: <US/Eastern>'
>>> d.dt
datetime.datetime(2008, 12, 30, 6, 0, tzinfo=<US/Eastern>)
>>> d.later
datetime.datetime(2008, 12, 30, 7, 0, tzinfo=<US/Eastern>)
>>> d.dt = d.dt.astimezone(pacific)
>>> d.tzinfo
<US/Pacific>
>>> d.compound
'OK: <US/Pacific>'

Separating timezone and point-in-time into different cells:

>>> class Dated2(Dated):
...     base_dt = trellis.attr(None)
...     tzinfo = trellis.attr(None)
...
...     @trellis.compute
...     def dt(self):
...         return self.base_dt.astimezone(self.tzinfo)
...
...     @trellis.maintain
...     def compound(self):
...         return "OK: %s" % self.dt

>>> d2 = Dated2(base_dt=d.dt, tzinfo=pacific)
>>> d2.dt
datetime.datetime(2008, 12, 30, 3, 0, tzinfo=<US/Pacific>)
>>> d2.tzinfo = eastern
>>> d2.dt #fails
datetime.datetime(2008, 12, 30, 6, 0, tzinfo=<US/Eastern>)

-------------------------------

Patch that gets the tests passing:

Index: peak/events/trellis.py
===================================================================
--- peak/events/trellis.py	(revision 2595)
+++ peak/events/trellis.py	(working copy)
@@ -118,6 +118,13 @@
         return who is not _sentinel and who is not self


+def strong_eq_test(value, other):
+    if value != other: return False
+    import datetime
+    if isinstance(value, datetime.datetime):
+        if value.tzinfo != other.tzinfo:
+            return False
+    return True



@@ -138,7 +145,7 @@
         if value is self._value:
             return  # no change, no foul...

-        if value!=self._value:
+        if not strong_eq_test(value, self._value):
             if self._set_by not in (ctrl.current_listener, self):
                 # already set by someone else
                 raise InputConflict(self._value, value) #self._set_by)
#, value, ctrl.current_listener) # XXX
@@ -195,7 +202,7 @@
             on_commit(self._finish)
         else:
             value = self.rule()
-            if value!=self._value:
+            if not strong_eq_test(value, self._value):
                 if self._set_by is _sentinel:
                     change_attr(self, '_set_by', self)
                     on_commit(self._finish)



More information about the PEAK mailing list