Make comparison and subtraction of aware objects ignore tzinfo if the
operands have identical tzinfo members (meaning object identity -- "is").
I misunderstood the intent here, reading wrong conclusion into
conflicting clues.
diff --git a/Doc/lib/libdatetime.tex b/Doc/lib/libdatetime.tex
index 1adbc8e..32c0f93 100644
--- a/Doc/lib/libdatetime.tex
+++ b/Doc/lib/libdatetime.tex
@@ -863,10 +863,15 @@
\begin{itemize}
\item
- comparison of \class{timetz} to timetz, where timetz1 is considered
- less than timetz2 when timetz1 precedes timetz2 in time, and
- where the \class{timetz} objects are first adjusted by subtracting
- their UTC offsets (obtained from \method{utcoffset()}).
+ comparison of \class{timetz} to \class{time} or \class{timetz},
+ where \var{a} is considered less than \var{b} when \var{a} precedes
+ \var{b} in time. If one comparand is naive and the other is aware,
+ \exception{TypeError} is raised. If both comparands are aware, and
+ have the same \member{tzinfo} member, the common \member{tzinfo}
+ member is ignored and the base times 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()}).
\item
hash, use as dict key
@@ -1011,11 +1016,13 @@
\item
datetimetz1 + timedelta -> datetimetz2
timedelta + datetimetz1 -> datetimetz2
+
The same as addition of \class{datetime} objects, except that
datetimetz2.tzinfo is set to datetimetz1.tzinfo.
\item
datetimetz1 - timedelta -> datetimetz2
+
The same as addition of \class{datetime} objects, except that
datetimetz2.tzinfo is set to datetimetz1.tzinfo.
@@ -1025,32 +1032,31 @@
\naive\_datetimetz1 - datetime2 -> timedelta
datetime1 - \naive\_datetimetz2 -> timedelta
- \item
- Subtraction of a \class{datetime} or datetimetz, from a
+ Subtraction of a \class{datetime} or \class{datetimetz}, from a
\class{datetime} or \class{datetimetz}, is defined only if both
- operands are \naive, or if both are aware. If one is aware and
- the other is \naive, \exception{TypeError} is raised.
+ operands are \naive, or if both are aware. If one is aware and the
+ other is \naive, \exception{TypeError} is raised.
- \item
- If both are \naive, subtraction acts as for \class{datetime}
- subtraction.
+ If both are \naive, or both are aware and have the same \member{tzinfo}
+ member, subtraction acts as for \class{datetime} subtraction.
- \item
- If both are aware \class{datetimetz} objects, a-b acts as if a and b were
- first converted to UTC datetimes (by subtracting \code{a.utcoffset()}
- minutes from a, and \code{b.utcoffset()} minutes from b), and then doing
+ If both are aware and have different \member{tzinfo} members,
+ \code{a-b} acts as if \var{a} and \var{b} were first converted to UTC
+ datetimes (by subtracting \code{a.utcoffset()} minutes from \var{a},
+ and \code{b.utcoffset()} minutes from \var{b}), and then doing
\class{datetime} subtraction, except that the implementation never
overflows.
\item
- Comparison of \class{datetimetz} to \class{datetime} or datetimetz. As for
- subtraction, comparison is defined only if both operands are
- \naive\ or both are aware. If both are \naive, comparison is as
- for \class{datetime} objects with the same date and time components.
- If both are aware, comparison acts as if both were converted to
- UTC datetimes first, except the the implementation never
- overflows. If one comparand is \naive\ and the other aware,
- \exception{TypeError} is raised.
+ comparison of \class{datetimetz} to \class{datetime} or
+ \class{datetimetz}, where \var{a} is considered less than \var{b}
+ when \var{a} precedes \var{b} in time. If one comparand is naive and
+ the other is aware, \exception{TypeError} is raised. If both
+ comparands are aware, and have the same \member{tzinfo} member,
+ the common \member{tzinfo} member is ignored and the base datetimes
+ 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()}).
\item
hash, use as dict key
diff --git a/Lib/test/test_datetime.py b/Lib/test/test_datetime.py
index a65e41c..dc58972 100644
--- a/Lib/test/test_datetime.py
+++ b/Lib/test/test_datetime.py
@@ -1657,9 +1657,8 @@
def test_aware_compare(self):
cls = self.theclass
- # Primarily trying to ensure that utcoffset() gets called even if
- # the comparands have the same tzinfo member. timetz comparison
- # didn't used to do so, although datetimetz comparison did.
+ # Ensure that utcoffset() gets ignored if the comparands have
+ # the same tzinfo member.
class OperandDependentOffset(tzinfo):
def utcoffset(self, t):
if t.minute < 10:
@@ -1674,6 +1673,16 @@
for x in d0, d1, d2:
for y in d0, d1, d2:
got = cmp(x, y)
+ expected = cmp(x.minute, y.minute)
+ self.assertEqual(got, expected)
+
+ # However, if they're different members, uctoffset is not ignored.
+ d0 = base.replace(minute=3, tzinfo=OperandDependentOffset())
+ d1 = base.replace(minute=9, tzinfo=OperandDependentOffset())
+ d2 = base.replace(minute=11, tzinfo=OperandDependentOffset())
+ for x in d0, d1, d2:
+ for y in d0, d1, d2:
+ got = cmp(x, y)
if (x is d0 or x is d1) and (y is d0 or y is d1):
expected = 0
elif x is y is d2:
@@ -1893,6 +1902,36 @@
self.assertRaises(ValueError, base.replace, second=100)
self.assertRaises(ValueError, base.replace, microsecond=1000000)
+ def test_mixed_compare(self):
+ t1 = time(1, 2, 3)
+ t2 = timetz(1, 2, 3)
+ self.assertEqual(t1, t2)
+ t2 = t2.replace(tzinfo=None)
+ self.assertEqual(t1, t2)
+ t2 = t2.replace(tzinfo=FixedOffset(None, ""))
+ self.assertEqual(t1, t2)
+ t2 = t2.replace(tzinfo=FixedOffset(0, ""))
+ self.assertRaises(TypeError, lambda: t1 == t2)
+
+ # In timetz w/ identical tzinfo objects, utcoffset is ignored.
+ class Varies(tzinfo):
+ def __init__(self):
+ self.offset = 22
+ def utcoffset(self, t):
+ self.offset += 1
+ return self.offset
+
+ v = Varies()
+ t1 = t2.replace(tzinfo=v)
+ t2 = t2.replace(tzinfo=v)
+ self.assertEqual(t1.utcoffset(), timedelta(minutes=23))
+ self.assertEqual(t2.utcoffset(), timedelta(minutes=24))
+ self.assertEqual(t1, t2)
+
+ # But if they're not identical, it isn't ignored.
+ t2 = t2.replace(tzinfo=Varies())
+ self.failUnless(t1 < t2) # t1's offset counter still going up
+
class TestDateTimeTZ(TestDateTime, TZInfoBase):
theclass = datetimetz
@@ -1971,7 +2010,7 @@
def utcoffset(self, dt): return 1440 # out of bounds
t1 = self.theclass(2, 2, 2, tzinfo=Bogus())
t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, ""))
- self.assertRaises(ValueError, lambda: t1 == t1)
+ self.assertRaises(ValueError, lambda: t1 == t2)
def test_pickling(self):
import pickle, cPickle
@@ -2389,10 +2428,8 @@
def test_aware_subtract(self):
cls = self.theclass
- # Primarily trying to ensure that utcoffset() gets called even if
- # the operands have the same tzinfo member. Subtraction didn't
- # used to do this, and it makes a difference for DST-aware tzinfo
- # instances.
+ # Ensure that utcoffset() is ignored when the operands have the
+ # same tzinfo member.
class OperandDependentOffset(tzinfo):
def utcoffset(self, t):
if t.minute < 10:
@@ -2407,6 +2444,18 @@
for x in d0, d1, d2:
for y in d0, d1, d2:
got = x - y
+ expected = timedelta(minutes=x.minute - y.minute)
+ self.assertEqual(got, expected)
+
+ # OTOH, if the tzinfo members are distinct, utcoffsets aren't
+ # ignored.
+ base = cls(8, 9, 10, 11, 12, 13, 14)
+ d0 = base.replace(minute=3, tzinfo=OperandDependentOffset())
+ d1 = base.replace(minute=9, tzinfo=OperandDependentOffset())
+ d2 = base.replace(minute=11, tzinfo=OperandDependentOffset())
+ for x in d0, d1, d2:
+ for y in d0, d1, d2:
+ got = x - y
if (x is d0 or x is d1) and (y is d0 or y is d1):
expected = timedelta(0)
elif x is y is d2:
@@ -2418,6 +2467,35 @@
expected = timedelta(minutes=0-(11-59))
self.assertEqual(got, expected)
+ def test_mixed_compare(self):
+ t1 = datetime(1, 2, 3, 4, 5, 6, 7)
+ t2 = datetimetz(1, 2, 3, 4, 5, 6, 7)
+ self.assertEqual(t1, t2)
+ t2 = t2.replace(tzinfo=None)
+ self.assertEqual(t1, t2)
+ t2 = t2.replace(tzinfo=FixedOffset(None, ""))
+ self.assertEqual(t1, t2)
+ t2 = t2.replace(tzinfo=FixedOffset(0, ""))
+ self.assertRaises(TypeError, lambda: t1 == t2)
+
+ # In datetimetz w/ identical tzinfo objects, utcoffset is ignored.
+ class Varies(tzinfo):
+ def __init__(self):
+ self.offset = 22
+ def utcoffset(self, t):
+ self.offset += 1
+ return self.offset
+
+ v = Varies()
+ t1 = t2.replace(tzinfo=v)
+ t2 = t2.replace(tzinfo=v)
+ self.assertEqual(t1.utcoffset(), timedelta(minutes=23))
+ self.assertEqual(t2.utcoffset(), timedelta(minutes=24))
+ self.assertEqual(t1, t2)
+
+ # But if they're not identical, it isn't ignored.
+ t2 = t2.replace(tzinfo=Varies())
+ self.failUnless(t1 < t2) # t1's offset counter still going up
def test_suite():
allsuites = [unittest.makeSuite(klass, 'test')
diff --git a/Modules/datetimemodule.c b/Modules/datetimemodule.c
index d7c6005..e460dee 100644
--- a/Modules/datetimemodule.c
+++ b/Modules/datetimemodule.c
@@ -3136,17 +3136,28 @@
other->ob_type->tp_name);
return NULL;
}
- n1 = classify_utcoffset((PyObject *)self, &offset1);
- assert(n1 != OFFSET_UNKNOWN);
- if (n1 == OFFSET_ERROR)
- return NULL;
+ /* Ignore utcoffsets if they have identical tzinfo members. This
+ * isn't an optimization, it's design. If utcoffset() doesn't ignore
+ * its argument, it may return different results for self and other
+ * even if they have identical tzinfo members, and we're deliberately
+ * suppressing that (possible) difference.
+ */
+ if (get_tzinfo_member((PyObject *)self) == get_tzinfo_member(other)) {
+ offset1 = offset2 = 0;
+ n1 = n2 = OFFSET_NAIVE;
+ }
+ else {
+ n1 = classify_utcoffset((PyObject *)self, &offset1);
+ assert(n1 != OFFSET_UNKNOWN);
+ if (n1 == OFFSET_ERROR)
+ return NULL;
- n2 = classify_utcoffset(other, &offset2);
- assert(n2 != OFFSET_UNKNOWN);
- if (n2 == OFFSET_ERROR)
- return NULL;
-
- /* If they're both naive, or both aware and have the same offsets,
+ n2 = classify_utcoffset(other, &offset2);
+ assert(n2 != OFFSET_UNKNOWN);
+ if (n2 == OFFSET_ERROR)
+ return NULL;
+ }
+ /* If they're both naive, or both aware and have the same offsets,
* we get off cheap. Note that if they're both naive, offset1 ==
* offset2 == 0 at this point.
*/
@@ -3656,15 +3667,27 @@
other->ob_type->tp_name);
return NULL;
}
- n1 = classify_utcoffset((PyObject *)self, &offset1);
- assert(n1 != OFFSET_UNKNOWN);
- if (n1 == OFFSET_ERROR)
- return NULL;
+ /* Ignore utcoffsets if they have identical tzinfo members. This
+ * isn't an optimization, it's design. If utcoffset() doesn't ignore
+ * its argument, it may return different results for self and other
+ * even if they have identical tzinfo members, and we're deliberately
+ * suppressing that (possible) difference.
+ */
+ if (get_tzinfo_member((PyObject *)self) == get_tzinfo_member(other)) {
+ offset1 = offset2 = 0;
+ n1 = n2 = OFFSET_NAIVE;
+ }
+ else {
+ n1 = classify_utcoffset((PyObject *)self, &offset1);
+ assert(n1 != OFFSET_UNKNOWN);
+ if (n1 == OFFSET_ERROR)
+ return NULL;
- n2 = classify_utcoffset(other, &offset2);
- assert(n2 != OFFSET_UNKNOWN);
- if (n2 == OFFSET_ERROR)
- return NULL;
+ n2 = classify_utcoffset(other, &offset2);
+ assert(n2 != OFFSET_UNKNOWN);
+ if (n2 == OFFSET_ERROR)
+ return NULL;
+ }
/* If they're both naive, or both aware and have the same offsets,
* we get off cheap. Note that if they're both naive, offset1 ==
@@ -4601,15 +4624,29 @@
int offset1, offset2;
PyDateTime_Delta *delta;
- n1 = classify_utcoffset(left, &offset1);
- assert(n1 != OFFSET_UNKNOWN);
- if (n1 == OFFSET_ERROR)
- return NULL;
+ /* Ignore utcoffsets if they have identical tzinfo
+ * members. This isn't an optimization, it's design.
+ * If utcoffset() doesn't ignore its argument, it may
+ * return different results for self and other even
+ * if they have identical tzinfo members, and we're
+ * deliberately suppressing that (possible) difference.
+ */
+ if (get_tzinfo_member(left) ==
+ get_tzinfo_member(right)) {
+ offset1 = offset2 = 0;
+ n1 = n2 = OFFSET_NAIVE;
+ }
+ else {
+ n1 = classify_utcoffset(left, &offset1);
+ assert(n1 != OFFSET_UNKNOWN);
+ if (n1 == OFFSET_ERROR)
+ return NULL;
- n2 = classify_utcoffset(right, &offset2);
- assert(n2 != OFFSET_UNKNOWN);
- if (n2 == OFFSET_ERROR)
- return NULL;
+ n2 = classify_utcoffset(right, &offset2);
+ assert(n2 != OFFSET_UNKNOWN);
+ if (n2 == OFFSET_ERROR)
+ return NULL;
+ }
if (n1 != n2) {
PyErr_SetString(PyExc_TypeError,