Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8d81a01

Browse files
committed
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.
1 parent cd63e61 commit 8d81a01

4 files changed

Lines changed: 75 additions & 1 deletion

File tree

Doc/lib/libdatetime.tex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,14 @@ \subsection{\class{date} Objects \label{datetime-date}}
381381
comparison of date to date, where date1 is considered less than
382382
date2 when date1 precedes date2 in time. In other words,
383383
date1 < date2 if and only if date1.toordinal() < date2.toordinal().
384+
\note{In order to stop comparison from falling back to the default
385+
scheme of comparing object addresses, date comparison
386+
normally raises \exception{TypeError} if the other comparand
387+
isn't also a \class{date} object. However, \code{NotImplemented}
388+
is returned instead if the other comparand has a
389+
\method{timetuple} attribute. This hook gives other kinds of
390+
date objects a chance at implementing mixed-type comparison.}
391+
384392

385393
\item
386394
hash, use as dict key
@@ -711,6 +719,14 @@ \subsection{\class{datetime} Objects \label{datetime-datetime}}
711719
are compared. If both comparands are aware and have different
712720
\member{tzinfo} members, the comparands are first adjusted by
713721
subtracting their UTC offsets (obtained from \code{self.utcoffset()}).
722+
\note{In order to stop comparison from falling back to the default
723+
scheme of comparing object addresses, datetime comparison
724+
normally raises \exception{TypeError} if the other comparand
725+
isn't also a \class{datetime} object. However,
726+
\code{NotImplemented} is returned instead if the other comparand
727+
has a \method{timetuple} attribute. This hook gives other
728+
kinds of date objects a chance at implementing mixed-type
729+
comparison.}
714730

715731
\item
716732
hash, use as dict key

Lib/test/test_datetime.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,44 @@ def test_compare(self):
880880
self.assertRaises(TypeError, lambda: badarg > t1)
881881
self.assertRaises(TypeError, lambda: badarg >= t1)
882882

883+
def test_mixed_compare(self):
884+
our = self.theclass(2000, 4, 5)
885+
self.assertRaises(TypeError, cmp, our, 1)
886+
self.assertRaises(TypeError, cmp, 1, our)
887+
888+
class AnotherDateTimeClass(object):
889+
def __cmp__(self, other):
890+
# Return "equal" so calling this can't be confused with
891+
# compare-by-address (which never says "equal" for distinct
892+
# objects).
893+
return 0
894+
895+
# This still errors, because date and datetime comparison raise
896+
# TypeError instead of NotImplemented when they don't know what to
897+
# do, in order to stop comparison from falling back to the default
898+
# compare-by-address.
899+
their = AnotherDateTimeClass()
900+
self.assertRaises(TypeError, cmp, our, their)
901+
# Oops: The next stab raises TypeError in the C implementation,
902+
# but not in the Python implementation of datetime. The difference
903+
# is due to that the Python implementation defines __cmp__ but
904+
# the C implementation defines tp_richcompare. This is more pain
905+
# to fix than it's worth, so commenting out the test.
906+
# self.assertEqual(cmp(their, our), 0)
907+
908+
# But date and datetime comparison return NotImplemented instead if the
909+
# other object has a timetuple attr. This gives the other object a
910+
# chance to do the comparison.
911+
class Comparable(AnotherDateTimeClass):
912+
def timetuple(self):
913+
return ()
914+
915+
their = Comparable()
916+
self.assertEqual(cmp(our, their), 0)
917+
self.assertEqual(cmp(their, our), 0)
918+
self.failUnless(our == their)
919+
self.failUnless(their == our)
920+
883921
def test_bool(self):
884922
# All dates are considered true.
885923
self.failUnless(self.theclass.min)

Misc/NEWS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ Extension modules
100100
useful behavior when the optional tinzo argument was specified. See
101101
also SF bug report <http://www.python.org/sf/660872>.
102102

103+
date and datetime comparison: In order to prevent comparison from
104+
falling back to the default compare-object-addresses strategy, these
105+
raised TypeError whenever they didn't understand the other object type.
106+
They still do, except when the other object has a "timetuple" attribute,
107+
in which case they return NotImplemented now. This gives other
108+
datetime objects (e.g., mxDateTime) a chance to intercept the
109+
comparison.
110+
103111
The constructors building a datetime from a timestamp could raise
104112
ValueError if the platform C localtime()/gmtime() inserted "leap
105113
seconds". Leap seconds are ignored now. On such platforms, it's

Modules/datetimemodule.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2437,8 +2437,15 @@ date_richcompare(PyDateTime_Date *self, PyObject *other, int op)
24372437
int diff;
24382438

24392439
if (! PyDate_Check(other)) {
2440+
if (PyObject_HasAttrString(other, "timetuple")) {
2441+
/* A hook for other kinds of date objects. */
2442+
Py_INCREF(Py_NotImplemented);
2443+
return Py_NotImplemented;
2444+
}
2445+
/* Stop this from falling back to address comparison. */
24402446
PyErr_Format(PyExc_TypeError,
2441-
"can't compare date to %s instance",
2447+
"can't compare '%s' to '%s'",
2448+
self->ob_type->tp_name,
24422449
other->ob_type->tp_name);
24432450
return NULL;
24442451
}
@@ -4018,6 +4025,11 @@ datetime_richcompare(PyDateTime_DateTime *self, PyObject *other, int op)
40184025
int offset1, offset2;
40194026

40204027
if (! PyDateTime_Check(other)) {
4028+
if (PyObject_HasAttrString(other, "timetuple")) {
4029+
/* A hook for other kinds of datetime objects. */
4030+
Py_INCREF(Py_NotImplemented);
4031+
return Py_NotImplemented;
4032+
}
40214033
/* Stop this from falling back to address comparison. */
40224034
PyErr_Format(PyExc_TypeError,
40234035
"can't compare '%s' to '%s'",

0 commit comments

Comments
 (0)