diff options
-rw-r--r-- | Doc/lib/libdatetime.tex | 16 | ||||
-rw-r--r-- | Lib/test/test_datetime.py | 38 | ||||
-rw-r--r-- | Misc/NEWS | 8 | ||||
-rw-r--r-- | Modules/datetimemodule.c | 14 |
4 files changed, 75 insertions, 1 deletions
diff --git a/Doc/lib/libdatetime.tex b/Doc/lib/libdatetime.tex index 058d6d5..693ae79 100644 --- a/Doc/lib/libdatetime.tex +++ b/Doc/lib/libdatetime.tex @@ -381,6 +381,14 @@ Supported operations: comparison of date to date, where date1 is considered less than date2 when date1 precedes date2 in time. In other words, date1 < date2 if and only if date1.toordinal() < date2.toordinal(). + \note{In order to stop comparison from falling back to the default + scheme of comparing object addresses, date comparison + normally raises \exception{TypeError} if the other comparand + isn't also a \class{date} object. However, \code{NotImplemented} + is returned instead if the other comparand has a + \method{timetuple} attribute. This hook gives other kinds of + date objects a chance at implementing mixed-type comparison.} + \item hash, use as dict key @@ -711,6 +719,14 @@ Supported operations: are compared. If both comparands are aware and have different \member{tzinfo} members, the comparands are first adjusted by subtracting their UTC offsets (obtained from \code{self.utcoffset()}). + \note{In order to stop comparison from falling back to the default + scheme of comparing object addresses, datetime comparison + normally raises \exception{TypeError} if the other comparand + isn't also a \class{datetime} object. However, + \code{NotImplemented} is returned instead if the other comparand + has a \method{timetuple} attribute. This hook gives other + kinds of date objects a chance at implementing mixed-type + comparison.} \item hash, use as dict key diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py index 0b9597a..995b6a0 100644 --- a/Lib/test/test_datetime.py +++ b/Lib/test/test_datetime.py @@ -880,6 +880,44 @@ class TestDate(unittest.TestCase): self.assertRaises(TypeError, lambda: badarg > t1) self.assertRaises(TypeError, lambda: badarg >= t1) + def test_mixed_compare(self): + our = self.theclass(2000, 4, 5) + self.assertRaises(TypeError, cmp, our, 1) + self.assertRaises(TypeError, cmp, 1, our) + + class AnotherDateTimeClass(object): + def __cmp__(self, other): + # Return "equal" so calling this can't be confused with + # compare-by-address (which never says "equal" for distinct + # objects). + return 0 + + # This still errors, because date and datetime comparison raise + # TypeError instead of NotImplemented when they don't know what to + # do, in order to stop comparison from falling back to the default + # compare-by-address. + their = AnotherDateTimeClass() + self.assertRaises(TypeError, cmp, our, their) + # Oops: The next stab raises TypeError in the C implementation, + # but not in the Python implementation of datetime. The difference + # is due to that the Python implementation defines __cmp__ but + # the C implementation defines tp_richcompare. This is more pain + # to fix than it's worth, so commenting out the test. + # self.assertEqual(cmp(their, our), 0) + + # But date and datetime comparison return NotImplemented instead if the + # other object has a timetuple attr. This gives the other object a + # chance to do the comparison. + class Comparable(AnotherDateTimeClass): + def timetuple(self): + return () + + their = Comparable() + self.assertEqual(cmp(our, their), 0) + self.assertEqual(cmp(their, our), 0) + self.failUnless(our == their) + self.failUnless(their == our) + def test_bool(self): # All dates are considered true. self.failUnless(self.theclass.min) @@ -100,6 +100,14 @@ Extension modules useful behavior when the optional tinzo argument was specified. See also SF bug report <http://www.python.org/sf/660872>. + date and datetime comparison: In order to prevent comparison from + falling back to the default compare-object-addresses strategy, these + raised TypeError whenever they didn't understand the other object type. + They still do, except when the other object has a "timetuple" attribute, + in which case they return NotImplemented now. This gives other + datetime objects (e.g., mxDateTime) a chance to intercept the + comparison. + The constructors building a datetime from a timestamp could raise ValueError if the platform C localtime()/gmtime() inserted "leap seconds". Leap seconds are ignored now. On such platforms, it's diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c index 6f0cf1e..31e006d 100644 --- a/Modules/datetimemodule.c +++ b/Modules/datetimemodule.c @@ -2437,8 +2437,15 @@ date_richcompare(PyDateTime_Date *self, PyObject *other, int op) int diff; if (! PyDate_Check(other)) { + if (PyObject_HasAttrString(other, "timetuple")) { + /* A hook for other kinds of date objects. */ + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } + /* Stop this from falling back to address comparison. */ PyErr_Format(PyExc_TypeError, - "can't compare date to %s instance", + "can't compare '%s' to '%s'", + self->ob_type->tp_name, other->ob_type->tp_name); return NULL; } @@ -4018,6 +4025,11 @@ datetime_richcompare(PyDateTime_DateTime *self, PyObject *other, int op) int offset1, offset2; if (! PyDateTime_Check(other)) { + if (PyObject_HasAttrString(other, "timetuple")) { + /* A hook for other kinds of datetime objects. */ + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; + } /* Stop this from falling back to address comparison. */ PyErr_Format(PyExc_TypeError, "can't compare '%s' to '%s'", |