]> granicus.if.org Git - python/commitdiff
Implemented a Wiki suggestion:
authorTim Peters <tim.peters@gmail.com>
Sun, 22 Dec 2002 03:43:39 +0000 (03:43 +0000)
committerTim Peters <tim.peters@gmail.com>
Sun, 22 Dec 2002 03:43:39 +0000 (03:43 +0000)
{timetz,datetimetz}.{utcoffset,dst}() now return a timedelta (or None)
instead of an int (or None).

tzinfo.{utcoffset,dst)() can now return a timedelta (or an int, or None).

Curiously, this was much easier to do in the C implementation than in the
Python implementation (which lives in the Zope3 code tree) -- the C code
already had lots of hair to extract C ints from offset objects, and used
C ints internally.

Lib/test/test_datetime.py
Modules/datetimemodule.c

index 9fd38f14bc7cfc807b338b542fadb2127959fb1d..ca26872e44abad997c761d1f2237055b00633f9a 100644 (file)
@@ -1458,35 +1458,120 @@ class TestTime(unittest.TestCase):
         self.failUnless(not cls(0))
         self.failUnless(not cls())
 
-
-class TestTimeTZ(TestTime):
-
-    theclass = timetz
-
-    def test_empty(self):
-        t = self.theclass()
-        self.assertEqual(t.hour, 0)
-        self.assertEqual(t.minute, 0)
-        self.assertEqual(t.second, 0)
-        self.assertEqual(t.microsecond, 0)
-        self.failUnless(t.tzinfo is None)
+# A mixin for classes with a tzinfo= argument.  Subclasses must define
+# theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever)
+# must be legit (which is true for timetz and datetimetz).
+class TZInfoBase(unittest.TestCase):
 
     def test_bad_tzinfo_classes(self):
-        tz = self.theclass
-        self.assertRaises(TypeError, tz, tzinfo=12)
+        cls = self.theclass
+        self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=12)
 
         class NiceTry(object):
             def __init__(self): pass
             def utcoffset(self, dt): pass
-        self.assertRaises(TypeError, tz, tzinfo=NiceTry)
+        self.assertRaises(TypeError, cls, 1, 1, 1, tzinfo=NiceTry)
 
         class BetterTry(tzinfo):
             def __init__(self): pass
             def utcoffset(self, dt): pass
         b = BetterTry()
-        t = tz(tzinfo=b)
+        t = cls(1, 1, 1, tzinfo=b)
         self.failUnless(t.tzinfo is b)
 
+    def test_utc_offset_out_of_bounds(self):
+        class Edgy(tzinfo):
+            def __init__(self, offset):
+                self.offset = offset
+            def utcoffset(self, dt):
+                return self.offset
+
+        cls = self.theclass
+        for offset, legit in ((-1440, False),
+                              (-1439, True),
+                              (1439, True),
+                              (1440, False)):
+            if cls is timetz:
+                t = cls(1, 2, 3, tzinfo=Edgy(offset))
+            elif cls is datetimetz:
+                t = cls(6, 6, 6, 1, 2, 3, tzinfo=Edgy(offset))
+            if legit:
+                aofs = abs(offset)
+                h, m = divmod(aofs, 60)
+                tag = "%c%02d:%02d" % (offset < 0 and '-' or '+', h, m)
+                if isinstance(t, datetimetz):
+                    t = t.timetz()
+                self.assertEqual(str(t), "01:02:03" + tag)
+            else:
+                self.assertRaises(ValueError, str, t)
+
+    def test_tzinfo_classes(self):
+        cls = self.theclass
+        class C1(tzinfo):
+            def utcoffset(self, dt): return None
+            def dst(self, dt): return None
+            def tzname(self, dt): return None
+        for t in (cls(1, 1, 1),
+                  cls(1, 1, 1, tzinfo=None),
+                  cls(1, 1, 1, tzinfo=C1())):
+            self.failUnless(t.utcoffset() is None)
+            self.failUnless(t.dst() is None)
+            self.failUnless(t.tzname() is None)
+
+        class C2(tzinfo):
+            def utcoffset(self, dt): return -1439
+            def dst(self, dt): return 1439
+            def tzname(self, dt): return "aname"
+        class C3(tzinfo):
+            def utcoffset(self, dt): return timedelta(minutes=-1439)
+            def dst(self, dt): return timedelta(minutes=1439)
+            def tzname(self, dt): return "aname"
+        for t in cls(1, 1, 1, tzinfo=C2()), cls(1, 1, 1, tzinfo=C3()):
+            self.assertEqual(t.utcoffset(), timedelta(minutes=-1439))
+            self.assertEqual(t.dst(), timedelta(minutes=1439))
+            self.assertEqual(t.tzname(), "aname")
+
+        # Wrong types.
+        class C4(tzinfo):
+            def utcoffset(self, dt): return "aname"
+            def dst(self, dt): return ()
+            def tzname(self, dt): return 0
+        t = cls(1, 1, 1, tzinfo=C4())
+        self.assertRaises(TypeError, t.utcoffset)
+        self.assertRaises(TypeError, t.dst)
+        self.assertRaises(TypeError, t.tzname)
+
+        # Offset out of range.
+        class C5(tzinfo):
+            def utcoffset(self, dt): return -1440
+            def dst(self, dt): return 1440
+        class C6(tzinfo):
+            def utcoffset(self, dt): return timedelta(hours=-24)
+            def dst(self, dt): return timedelta(hours=24)
+        for t in cls(1, 1, 1, tzinfo=C5()), cls(1, 1, 1, tzinfo=C6()):
+            self.assertRaises(ValueError, t.utcoffset)
+            self.assertRaises(ValueError, t.dst)
+
+        # Not a whole number of minutes.
+        class C7(tzinfo):
+            def utcoffset(self, dt): return timedelta(seconds=61)
+            def dst(self, dt): return timedelta(microseconds=-81)
+        t = cls(1, 1, 1, tzinfo=C7())
+        self.assertRaises(ValueError, t.utcoffset)
+        self.assertRaises(ValueError, t.dst)
+
+
+class TestTimeTZ(TestTime, TZInfoBase):
+    theclass = timetz
+
+    def test_empty(self):
+        t = self.theclass()
+        self.assertEqual(t.hour, 0)
+        self.assertEqual(t.minute, 0)
+        self.assertEqual(t.second, 0)
+        self.assertEqual(t.microsecond, 0)
+        self.failUnless(t.tzinfo is None)
+
     def test_zones(self):
         est = FixedOffset(-300, "EST", 1)
         utc = FixedOffset(0, "UTC", -2)
@@ -1503,9 +1588,9 @@ class TestTimeTZ(TestTime):
         self.failUnless(t4.tzinfo is None)
         self.assertEqual(t5.tzinfo, utc)
 
-        self.assertEqual(t1.utcoffset(), -300)
-        self.assertEqual(t2.utcoffset(), 0)
-        self.assertEqual(t3.utcoffset(), 60)
+        self.assertEqual(t1.utcoffset(), timedelta(minutes=-300))
+        self.assertEqual(t2.utcoffset(), timedelta(minutes=0))
+        self.assertEqual(t3.utcoffset(), timedelta(minutes=60))
         self.failUnless(t4.utcoffset() is None)
         self.assertRaises(TypeError, t1.utcoffset, "no args")
 
@@ -1515,9 +1600,9 @@ class TestTimeTZ(TestTime):
         self.failUnless(t4.tzname() is None)
         self.assertRaises(TypeError, t1.tzname, "no args")
 
-        self.assertEqual(t1.dst(), 1)
-        self.assertEqual(t2.dst(), -2)
-        self.assertEqual(t3.dst(), 3)
+        self.assertEqual(t1.dst(), timedelta(minutes=1))
+        self.assertEqual(t2.dst(), timedelta(minutes=-2))
+        self.assertEqual(t3.dst(), timedelta(minutes=3))
         self.failUnless(t4.dst() is None)
         self.assertRaises(TypeError, t1.dst, "no args")
 
@@ -1578,26 +1663,6 @@ class TestTimeTZ(TestTime):
         t2 = self.theclass(23, 48, 6, 100, tzinfo=FixedOffset(-1010, ""))
         self.assertEqual(hash(t1), hash(t2))
 
-    def test_utc_offset_out_of_bounds(self):
-        class Edgy(tzinfo):
-            def __init__(self, offset):
-                self.offset = offset
-            def utcoffset(self, dt):
-                return self.offset
-
-        for offset, legit in ((-1440, False),
-                              (-1439, True),
-                              (1439, True),
-                              (1440, False)):
-            t = timetz(1, 2, 3, tzinfo=Edgy(offset))
-            if legit:
-                aofs = abs(offset)
-                h, m = divmod(aofs, 60)
-                tag = "%c%02d:%02d" % (offset < 0 and '-' or '+', h, m)
-                self.assertEqual(str(t), "01:02:03" + tag)
-            else:
-                self.assertRaises(ValueError, str, t)
-
     def test_pickling(self):
         import pickle, cPickle
 
@@ -1623,7 +1688,7 @@ class TestTimeTZ(TestTime):
         derived.__setstate__(state)
         self.assertEqual(orig, derived)
         self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset))
-        self.assertEqual(derived.utcoffset(), -300)
+        self.assertEqual(derived.utcoffset(), timedelta(minutes=-300))
         self.assertEqual(derived.tzname(), 'cookie')
 
         for pickler in pickle, cPickle:
@@ -1633,7 +1698,7 @@ class TestTimeTZ(TestTime):
                 self.assertEqual(orig, derived)
                 self.failUnless(isinstance(derived.tzinfo,
                                 PicklableFixedOffset))
-                self.assertEqual(derived.utcoffset(), -300)
+                self.assertEqual(derived.utcoffset(), timedelta(minutes=-300))
                 self.assertEqual(derived.tzname(), 'cookie')
 
     def test_more_bool(self):
@@ -1664,8 +1729,7 @@ class TestTimeTZ(TestTime):
         t = cls(0, tzinfo=FixedOffset(-24*60, ""))
         self.assertRaises(ValueError, lambda: bool(t))
 
-class TestDateTimeTZ(TestDateTime):
-
+class TestDateTimeTZ(TestDateTime, TZInfoBase):
     theclass = datetimetz
 
     def test_trivial(self):
@@ -1744,22 +1808,6 @@ class TestDateTimeTZ(TestDateTime):
         t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, ""))
         self.assertRaises(ValueError, lambda: t1 == t1)
 
-    def test_bad_tzinfo_classes(self):
-        tz = self.theclass
-        self.assertRaises(TypeError, tz, 1, 2, 3, tzinfo=12)
-
-        class NiceTry(object):
-            def __init__(self): pass
-            def utcoffset(self, dt): pass
-        self.assertRaises(TypeError, tz, 1, 2, 3, tzinfo=NiceTry)
-
-        class BetterTry(tzinfo):
-            def __init__(self): pass
-            def utcoffset(self, dt): pass
-        b = BetterTry()
-        t = tz(1, 2, 3, tzinfo=b)
-        self.failUnless(t.tzinfo is b)
-
     def test_pickling(self):
         import pickle, cPickle
 
@@ -1785,7 +1833,7 @@ class TestDateTimeTZ(TestDateTime):
         derived.__setstate__(state)
         self.assertEqual(orig, derived)
         self.failUnless(isinstance(derived.tzinfo, PicklableFixedOffset))
-        self.assertEqual(derived.utcoffset(), -300)
+        self.assertEqual(derived.utcoffset(), timedelta(minutes=-300))
         self.assertEqual(derived.tzname(), 'cookie')
 
         for pickler in pickle, cPickle:
@@ -1795,7 +1843,7 @@ class TestDateTimeTZ(TestDateTime):
                 self.assertEqual(orig, derived)
                 self.failUnless(isinstance(derived.tzinfo,
                                 PicklableFixedOffset))
-                self.assertEqual(derived.utcoffset(), -300)
+                self.assertEqual(derived.utcoffset(), timedelta(minutes=-300))
                 self.assertEqual(derived.tzname(), 'cookie')
 
     def test_extreme_hashes(self):
@@ -1822,9 +1870,9 @@ class TestDateTimeTZ(TestDateTime):
         self.assertEqual(t1.tzinfo, est)
         self.assertEqual(t2.tzinfo, utc)
         self.assertEqual(t3.tzinfo, met)
-        self.assertEqual(t1.utcoffset(), -300)
-        self.assertEqual(t2.utcoffset(), 0)
-        self.assertEqual(t3.utcoffset(), 60)
+        self.assertEqual(t1.utcoffset(), timedelta(minutes=-300))
+        self.assertEqual(t2.utcoffset(), timedelta(minutes=0))
+        self.assertEqual(t3.utcoffset(), timedelta(minutes=60))
         self.assertEqual(t1.tzname(), "EST")
         self.assertEqual(t2.tzname(), "UTC")
         self.assertEqual(t3.tzname(), "MET")
@@ -1914,8 +1962,7 @@ class TestDateTimeTZ(TestDateTime):
         #            (nowaware base - nowawareplus base) +
         #            (nowawareplus offset - nowaware offset) =
         #            -delta + nowawareplus offset - nowaware offset
-        expected = timedelta(minutes=nowawareplus.utcoffset() -
-                                     nowaware.utcoffset()) - delta
+        expected = nowawareplus.utcoffset() - nowaware.utcoffset() - delta
         self.assertEqual(got, expected)
 
         # Try max possible difference.
@@ -1935,7 +1982,7 @@ class TestDateTimeTZ(TestDateTime):
         another = meth(off42)
         again = meth(tzinfo=off42)
         self.failUnless(another.tzinfo is again.tzinfo)
-        self.assertEqual(another.utcoffset(), 42)
+        self.assertEqual(another.utcoffset(), timedelta(minutes=42))
         # Bad argument with and w/o naming the keyword.
         self.assertRaises(TypeError, meth, 16)
         self.assertRaises(TypeError, meth, tzinfo=16)
@@ -1955,7 +2002,7 @@ class TestDateTimeTZ(TestDateTime):
         another = meth(ts, off42)
         again = meth(ts, tzinfo=off42)
         self.failUnless(another.tzinfo is again.tzinfo)
-        self.assertEqual(another.utcoffset(), 42)
+        self.assertEqual(another.utcoffset(), timedelta(minutes=42))
         # Bad argument with and w/o naming the keyword.
         self.assertRaises(TypeError, meth, ts, 16)
         self.assertRaises(TypeError, meth, ts, tzinfo=16)
index 6dff6594b7a3561760057c09174671a83952214e..9a9ab7e1f412dbb7f2aa659e2cdea1a2735bc7ad 100644 (file)
@@ -549,6 +549,40 @@ normalize_datetime(int *year, int *month, int *day,
  * tzinfo helpers.
  */
 
+/* Ensure that p is None or of a tzinfo subclass.  Return 0 if OK; if not
+ * raise TypeError and return -1.
+ */
+static int
+check_tzinfo_subclass(PyObject *p)
+{
+       if (p == Py_None || PyTZInfo_Check(p))
+               return 0;
+       PyErr_Format(PyExc_TypeError,
+                    "tzinfo argument must be None or of a tzinfo subclass, "
+                    "not type '%s'",
+                    p->ob_type->tp_name);
+       return -1;
+}
+
+/* Return tzinfo.methname(self), without any checking of results.
+ * If tzinfo is None, returns None.
+ */
+static PyObject *
+call_tzinfo_method(PyObject *self, PyObject *tzinfo, char *methname)
+{
+       PyObject *result;
+
+       assert(self && tzinfo && methname);
+       assert(check_tzinfo_subclass(tzinfo) >= 0);
+       if (tzinfo == Py_None) {
+               result = Py_None;
+               Py_INCREF(result);
+       }
+       else
+               result = PyObject_CallMethod(tzinfo, methname, "O", self);
+       return result;
+}
+
 /* If self has a tzinfo member, return a BORROWED reference to it.  Else
  * return NULL, which is NOT AN ERROR.  There are no error returns here,
  * and the caller must not decref the result.
@@ -566,28 +600,15 @@ get_tzinfo_member(PyObject *self)
        return tzinfo;
 }
 
-/* Ensure that p is None or of a tzinfo subclass.  Return 0 if OK; if not
- * raise TypeError and return -1.
- */
-static int
-check_tzinfo_subclass(PyObject *p)
-{
-       if (p == Py_None || PyTZInfo_Check(p))
-               return 0;
-       PyErr_Format(PyExc_TypeError,
-                    "tzinfo argument must be None or of a tzinfo subclass, "
-                    "not type '%s'",
-                    p->ob_type->tp_name);
-       return -1;
-}
-
 /* Internal helper.
  * Call getattr(tzinfo, name)(tzinfoarg), and extract an int from the
  * result.  tzinfo must be an instance of the tzinfo class.  If the method
  * returns None, this returns 0 and sets *none to 1.  If the method doesn't
- * return a Python int or long, TypeError is raised and this returns -1.
- * If it does return an int or long, but is outside the valid range for
- * a UTC minute offset, ValueError is raised and this returns -1.
+ * return a Python int or long or timedelta, TypeError is raised and this
+ * returns -1.  If it returns an int or long, but is outside the valid
+ * range for a UTC minute offset, or it returns a timedelta and the value is
+ * out of range or isn't a whole number of minutes, ValueError is raised and
+ * this returns -1.
  * Else *none is set to 0 and the integer method result is returned.
  */
 static int
@@ -602,7 +623,7 @@ call_utc_tzinfo_method(PyObject *tzinfo, char *name, PyObject *tzinfoarg,
        assert(tzinfoarg != NULL);
 
        *none = 0;
-       u = PyObject_CallMethod(tzinfo, name, "O", tzinfoarg);
+       u = call_tzinfo_method(tzinfoarg, tzinfo, name);
        if (u == NULL)
                return -1;
 
@@ -614,12 +635,35 @@ call_utc_tzinfo_method(PyObject *tzinfo, char *name, PyObject *tzinfoarg,
 
        if (PyInt_Check(u))
                result = PyInt_AS_LONG(u);
+
        else if (PyLong_Check(u))
                result = PyLong_AsLong(u);
+
+       else if (PyDelta_Check(u)) {
+               const int days = GET_TD_DAYS(u);
+               if (days < -1 || days > 0)
+                       result = 24*60; /* trigger ValueError below */
+               else {
+                       /* next line can't overflow because we know days
+                        * is -1 or 0 now
+                        */
+                       int ss = days * 24 * 3600 + GET_TD_SECONDS(u);
+                       result = divmod(ss, 60, &ss);
+                       if (ss || GET_TD_MICROSECONDS(u)) {
+                               PyErr_Format(PyExc_ValueError,
+                                            "tzinfo.%s() must return a "
+                                            "whole number of minutes",
+                                            name);
+                               result = -1;
+                               goto Done;
+                       }
+               }
+       }
        else {
                PyErr_Format(PyExc_TypeError,
-                            "tzinfo.%s() must return None or int or long",
-                            name);
+                            "tzinfo.%s() must return None, integer or "
+                            "timedelta, not '%s'",
+                            name, u->ob_type->tp_name);
                goto Done;
        }
 
@@ -649,6 +693,32 @@ call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg, int *none)
        return call_utc_tzinfo_method(tzinfo, "utcoffset", tzinfoarg, none);
 }
 
+static PyObject *new_delta(int d, int sec, int usec, int normalize);
+
+/* Call tzinfo.name(self) and return the offset as a timedelta or None. */
+static PyObject *
+offset_as_timedelta(PyObject *self, PyObject *tzinfo, char *name) {
+       PyObject *result;
+
+       if (tzinfo == Py_None) {
+               result = Py_None;
+               Py_INCREF(result);
+       }
+       else {
+               int none;
+               int offset = call_utc_tzinfo_method(tzinfo, name, self, &none);
+               if (offset < 0 && PyErr_Occurred())
+                       return NULL;
+               if (none) {
+                       result = Py_None;
+                       Py_INCREF(result);
+               }
+               else
+                       result = new_delta(0, offset * 60, 0, 1);
+       }
+       return result;
+}
+
 /* Call tzinfo.dst(tzinfoarg), and extract an integer from the
  * result.  tzinfo must be an instance of the tzinfo class.  If dst()
  * returns None, call_dst returns 0 and sets *none to 1.  If dst()
@@ -663,22 +733,29 @@ call_dst(PyObject *tzinfo, PyObject *tzinfoarg, int *none)
        return call_utc_tzinfo_method(tzinfo, "dst", tzinfoarg, none);
 }
 
-/* Call tzinfo.tzname(tzinfoarg), and return the result.  tzinfo must be
- * an instance of the tzinfo class.  If tzname() doesn't return None or
- * a string, TypeError is raised and this returns NULL.
+/* Call tzinfo.tzname(self), and return the result.  tzinfo must be
+ * an instance of the tzinfo class or None.  If tzinfo isn't None, and
+ * tzname() doesn't return None ora string, TypeError is raised and this
+ * returns NULL.
  */
 static PyObject *
-call_tzname(PyObject *tzinfo, PyObject *tzinfoarg)
+call_tzname(PyObject *self, PyObject *tzinfo)
 {
        PyObject *result;
 
+       assert(self != NULL);
        assert(tzinfo != NULL);
-       assert(PyTZInfo_Check(tzinfo));
-       assert(tzinfoarg != NULL);
+       assert(check_tzinfo_subclass(tzinfo) >= 0);
 
-       result = PyObject_CallMethod(tzinfo, "tzname", "O", tzinfoarg);
-       if (result != NULL && result != Py_None && !PyString_Check(result)) {
-               PyErr_Format(PyExc_TypeError, ".tzinfo.tzname() must "
+       if (tzinfo == Py_None) {
+               result = Py_None;
+               Py_INCREF(result);
+       }
+       else
+               result = PyObject_CallMethod(tzinfo, "tzname", "O", self);
+
+       if (result != NULL && result != Py_None && ! PyString_Check(result)) {
+               PyErr_Format(PyExc_TypeError, "tzinfo.tzname() must "
                             "return None or a string, not '%s'",
                             result->ob_type->tp_name);
                Py_DECREF(result);
@@ -699,7 +776,7 @@ typedef enum {
              /* date,
               * datetime,
               * datetimetz with None tzinfo,
-              * datetimetz where utcoffset() return None
+              * datetimetz where utcoffset() returns None
               * time,
               * timetz with None tzinfo,
               * timetz where utcoffset() returns None
@@ -919,8 +996,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple)
                                Zreplacement = PyString_FromString("");
                                if (Zreplacement == NULL) goto Done;
                                if (tzinfo != Py_None && tzinfo != NULL) {
-                                       PyObject *temp = call_tzname(tzinfo,
-                                                                    object);
+                                       PyObject *temp = call_tzname(object,
+                                                                    tzinfo);
                                        if (temp == NULL) goto Done;
                                        if (temp != Py_None) {
                                                assert(PyString_Check(temp));
@@ -3917,38 +3994,24 @@ timetz_dealloc(PyDateTime_TimeTZ *self)
 }
 
 /*
- * Indirect access to tzinfo methods.  One more "convenience function" and
- * it won't be possible to find the useful methods anymore <0.5 wink>.
+ * Indirect access to tzinfo methods.
  */
 
-static PyObject *
-timetz_convienience(PyDateTime_TimeTZ *self, char *name)
-{
-       PyObject *result;
-
-       if (self->tzinfo == Py_None) {
-               result = Py_None;
-               Py_INCREF(result);
-       }
-       else
-               result = PyObject_CallMethod(self->tzinfo, name, "O", self);
-       return result;
-}
-
 /* These are all METH_NOARGS, so don't need to check the arglist. */
 static PyObject *
 timetz_utcoffset(PyDateTime_TimeTZ *self, PyObject *unused) {
-       return timetz_convienience(self, "utcoffset");
+       return offset_as_timedelta((PyObject *)self, self->tzinfo,
+                                  "utcoffset");
 }
 
 static PyObject *
-timetz_tzname(PyDateTime_TimeTZ *self, PyObject *unused) {
-       return timetz_convienience(self, "tzname");
+timetz_dst(PyDateTime_TimeTZ *self, PyObject *unused) {
+       return offset_as_timedelta((PyObject *)self, self->tzinfo, "dst");
 }
 
 static PyObject *
-timetz_dst(PyDateTime_TimeTZ *self, PyObject *unused) {
-       return timetz_convienience(self, "dst");
+timetz_tzname(PyDateTime_TimeTZ *self, PyObject *unused) {
+       return call_tzname((PyObject *)self, self->tzinfo);
 }
 
 /*
@@ -4325,37 +4388,21 @@ datetimetz_dealloc(PyDateTime_DateTimeTZ *self)
  * Indirect access to tzinfo methods.
  */
 
-/* Internal helper.
- * Call a tzinfo object's method, or return None if tzinfo is None.
- */
-static PyObject *
-datetimetz_convienience(PyDateTime_DateTimeTZ *self, char *name)
-{
-       PyObject *result;
-
-       if (self->tzinfo == Py_None) {
-               result = Py_None;
-               Py_INCREF(result);
-       }
-       else
-               result = PyObject_CallMethod(self->tzinfo, name, "O", self);
-       return result;
-}
-
 /* These are all METH_NOARGS, so don't need to check the arglist. */
 static PyObject *
 datetimetz_utcoffset(PyDateTime_DateTimeTZ *self, PyObject *unused) {
-       return datetimetz_convienience(self, "utcoffset");
+       return offset_as_timedelta((PyObject *)self, self->tzinfo,
+                                  "utcoffset");
 }
 
 static PyObject *
-datetimetz_tzname(PyDateTime_DateTimeTZ *self, PyObject *unused) {
-       return datetimetz_convienience(self, "tzname");
+datetimetz_dst(PyDateTime_DateTimeTZ *self, PyObject *unused) {
+       return offset_as_timedelta((PyObject *)self, self->tzinfo, "dst");
 }
 
 static PyObject *
-datetimetz_dst(PyDateTime_DateTimeTZ *self, PyObject *unused) {
-       return datetimetz_convienience(self, "dst");
+datetimetz_tzname(PyDateTime_DateTimeTZ *self, PyObject *unused) {
+       return call_tzname((PyObject *)self, self->tzinfo);
 }
 
 /*