From 8d81a012efa7fde5e43b8ea7275c7fc995cf74fa Mon Sep 17 00:00:00 2001 From: Tim Peters Date: Fri, 24 Jan 2003 22:36:34 +0000 Subject: date and datetime comparison: when we don't know how to compare against "the other" argument, we raise TypeError, in order to prevent comparison from falling back to the default (and worse than useless, in this case) comparison by object address. That's fine so far as it goes, but leaves no way for another date/datetime object to make itself comparable to our objects. For example, it leaves Marc-Andre no way to teach mxDateTime dates how to compare against Python dates. Discussion on Python-Dev raised a number of impractical ideas, and the simple one implemented here: when we don't know how to compare against "the other" argument, we raise TypeError *unless* the other object has a timetuple attr. In that case, we return NotImplemented instead, and Python will give the other object a shot at handling the comparison then. Note that comparisons of time and timedelta objects still suffer the original problem, though. --- Doc/lib/libdatetime.tex | 16 ++++++++++++++++ Lib/test/test_datetime.py | 38 ++++++++++++++++++++++++++++++++++++++ Misc/NEWS | 8 ++++++++ Modules/datetimemodule.c | 14 +++++++++++++- 4 files changed, 75 insertions(+), 1 deletion(-) 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) diff --git a/Misc/NEWS b/Misc/NEWS index ac61241..d63d522 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -100,6 +100,14 @@ Extension modules useful behavior when the optional tinzo argument was specified. See also SF bug report . + 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'", -- cgit v0.12