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.
This commit is contained in:
Tim Peters 2002-12-27 00:41:11 +00:00
parent f044e090c4
commit 60c76e4016
3 changed files with 178 additions and 57 deletions

View File

@ -863,10 +863,15 @@ Supported operations:
\begin{itemize} \begin{itemize}
\item \item
comparison of \class{timetz} to timetz, where timetz1 is considered comparison of \class{timetz} to \class{time} or \class{timetz},
less than timetz2 when timetz1 precedes timetz2 in time, and where \var{a} is considered less than \var{b} when \var{a} precedes
where the \class{timetz} objects are first adjusted by subtracting \var{b} in time. If one comparand is naive and the other is aware,
their UTC offsets (obtained from \method{utcoffset()}). \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 \item
hash, use as dict key hash, use as dict key
@ -1011,11 +1016,13 @@ Supported operations:
\item \item
datetimetz1 + timedelta -> datetimetz2 datetimetz1 + timedelta -> datetimetz2
timedelta + datetimetz1 -> datetimetz2 timedelta + datetimetz1 -> datetimetz2
The same as addition of \class{datetime} objects, except that The same as addition of \class{datetime} objects, except that
datetimetz2.tzinfo is set to datetimetz1.tzinfo. datetimetz2.tzinfo is set to datetimetz1.tzinfo.
\item \item
datetimetz1 - timedelta -> datetimetz2 datetimetz1 - timedelta -> datetimetz2
The same as addition of \class{datetime} objects, except that The same as addition of \class{datetime} objects, except that
datetimetz2.tzinfo is set to datetimetz1.tzinfo. datetimetz2.tzinfo is set to datetimetz1.tzinfo.
@ -1025,32 +1032,31 @@ Supported operations:
\naive\_datetimetz1 - datetime2 -> timedelta \naive\_datetimetz1 - datetime2 -> timedelta
datetime1 - \naive\_datetimetz2 -> timedelta datetime1 - \naive\_datetimetz2 -> timedelta
\item Subtraction of a \class{datetime} or \class{datetimetz}, from a
Subtraction of a \class{datetime} or datetimetz, from a
\class{datetime} or \class{datetimetz}, is defined only if both \class{datetime} or \class{datetimetz}, is defined only if both
operands are \naive, or if both are aware. If one is aware and operands are \naive, or if both are aware. If one is aware and the
the other is \naive, \exception{TypeError} is raised. other is \naive, \exception{TypeError} is raised.
\item If both are \naive, or both are aware and have the same \member{tzinfo}
If both are \naive, subtraction acts as for \class{datetime} member, subtraction acts as for \class{datetime} subtraction.
subtraction.
\item If both are aware and have different \member{tzinfo} members,
If both are aware \class{datetimetz} objects, a-b acts as if a and b were \code{a-b} acts as if \var{a} and \var{b} were first converted to UTC
first converted to UTC datetimes (by subtracting \code{a.utcoffset()} datetimes (by subtracting \code{a.utcoffset()} minutes from \var{a},
minutes from a, and \code{b.utcoffset()} minutes from b), and then doing and \code{b.utcoffset()} minutes from \var{b}), and then doing
\class{datetime} subtraction, except that the implementation never \class{datetime} subtraction, except that the implementation never
overflows. overflows.
\item \item
Comparison of \class{datetimetz} to \class{datetime} or datetimetz. As for comparison of \class{datetimetz} to \class{datetime} or
subtraction, comparison is defined only if both operands are \class{datetimetz}, where \var{a} is considered less than \var{b}
\naive\ or both are aware. If both are \naive, comparison is as when \var{a} precedes \var{b} in time. If one comparand is naive and
for \class{datetime} objects with the same date and time components. the other is aware, \exception{TypeError} is raised. If both
If both are aware, comparison acts as if both were converted to comparands are aware, and have the same \member{tzinfo} member,
UTC datetimes first, except the the implementation never the common \member{tzinfo} member is ignored and the base datetimes
overflows. If one comparand is \naive\ and the other aware, are compared. If both comparands are aware and have different
\exception{TypeError} is raised. \member{tzinfo} members, the comparands are first adjusted by
subtracting their UTC offsets (obtained from \code{self.utcoffset()}).
\item \item
hash, use as dict key hash, use as dict key

View File

@ -1657,9 +1657,8 @@ class TZInfoBase(unittest.TestCase):
def test_aware_compare(self): def test_aware_compare(self):
cls = self.theclass cls = self.theclass
# Primarily trying to ensure that utcoffset() gets called even if # Ensure that utcoffset() gets ignored if the comparands have
# the comparands have the same tzinfo member. timetz comparison # the same tzinfo member.
# didn't used to do so, although datetimetz comparison did.
class OperandDependentOffset(tzinfo): class OperandDependentOffset(tzinfo):
def utcoffset(self, t): def utcoffset(self, t):
if t.minute < 10: if t.minute < 10:
@ -1671,6 +1670,16 @@ class TZInfoBase(unittest.TestCase):
d0 = base.replace(minute=3) d0 = base.replace(minute=3)
d1 = base.replace(minute=9) d1 = base.replace(minute=9)
d2 = base.replace(minute=11) d2 = base.replace(minute=11)
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 x in d0, d1, d2:
for y in d0, d1, d2: for y in d0, d1, d2:
got = cmp(x, y) got = cmp(x, y)
@ -1893,6 +1902,36 @@ class TestTimeTZ(TestTime, TZInfoBase):
self.assertRaises(ValueError, base.replace, second=100) self.assertRaises(ValueError, base.replace, second=100)
self.assertRaises(ValueError, base.replace, microsecond=1000000) 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): class TestDateTimeTZ(TestDateTime, TZInfoBase):
theclass = datetimetz theclass = datetimetz
@ -1971,7 +2010,7 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
def utcoffset(self, dt): return 1440 # out of bounds def utcoffset(self, dt): return 1440 # out of bounds
t1 = self.theclass(2, 2, 2, tzinfo=Bogus()) t1 = self.theclass(2, 2, 2, tzinfo=Bogus())
t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, "")) 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): def test_pickling(self):
import pickle, cPickle import pickle, cPickle
@ -2389,10 +2428,8 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
def test_aware_subtract(self): def test_aware_subtract(self):
cls = self.theclass cls = self.theclass
# Primarily trying to ensure that utcoffset() gets called even if # Ensure that utcoffset() is ignored when the operands have the
# the operands have the same tzinfo member. Subtraction didn't # same tzinfo member.
# used to do this, and it makes a difference for DST-aware tzinfo
# instances.
class OperandDependentOffset(tzinfo): class OperandDependentOffset(tzinfo):
def utcoffset(self, t): def utcoffset(self, t):
if t.minute < 10: if t.minute < 10:
@ -2404,6 +2441,18 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
d0 = base.replace(minute=3) d0 = base.replace(minute=3)
d1 = base.replace(minute=9) d1 = base.replace(minute=9)
d2 = base.replace(minute=11) d2 = base.replace(minute=11)
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 x in d0, d1, d2:
for y in d0, d1, d2: for y in d0, d1, d2:
got = x - y got = x - y
@ -2418,6 +2467,35 @@ class TestDateTimeTZ(TestDateTime, TZInfoBase):
expected = timedelta(minutes=0-(11-59)) expected = timedelta(minutes=0-(11-59))
self.assertEqual(got, expected) 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(): def test_suite():
allsuites = [unittest.makeSuite(klass, 'test') allsuites = [unittest.makeSuite(klass, 'test')

View File

@ -3136,17 +3136,28 @@ datetime_richcompare(PyDateTime_DateTime *self, PyObject *other, int op)
other->ob_type->tp_name); other->ob_type->tp_name);
return NULL; return NULL;
} }
n1 = classify_utcoffset((PyObject *)self, &offset1); /* Ignore utcoffsets if they have identical tzinfo members. This
assert(n1 != OFFSET_UNKNOWN); * isn't an optimization, it's design. If utcoffset() doesn't ignore
if (n1 == OFFSET_ERROR) * its argument, it may return different results for self and other
return NULL; * 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); n2 = classify_utcoffset(other, &offset2);
assert(n2 != OFFSET_UNKNOWN); assert(n2 != OFFSET_UNKNOWN);
if (n2 == OFFSET_ERROR) if (n2 == OFFSET_ERROR)
return NULL; return NULL;
}
/* If they're both naive, or both aware and have the same offsets, /* 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 == * we get off cheap. Note that if they're both naive, offset1 ==
* offset2 == 0 at this point. * offset2 == 0 at this point.
*/ */
@ -3656,15 +3667,27 @@ time_richcompare(PyDateTime_Time *self, PyObject *other, int op)
other->ob_type->tp_name); other->ob_type->tp_name);
return NULL; return NULL;
} }
n1 = classify_utcoffset((PyObject *)self, &offset1); /* Ignore utcoffsets if they have identical tzinfo members. This
assert(n1 != OFFSET_UNKNOWN); * isn't an optimization, it's design. If utcoffset() doesn't ignore
if (n1 == OFFSET_ERROR) * its argument, it may return different results for self and other
return NULL; * 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); n2 = classify_utcoffset(other, &offset2);
assert(n2 != OFFSET_UNKNOWN); assert(n2 != OFFSET_UNKNOWN);
if (n2 == OFFSET_ERROR) if (n2 == OFFSET_ERROR)
return NULL; return NULL;
}
/* If they're both naive, or both aware and have the same offsets, /* 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 == * we get off cheap. Note that if they're both naive, offset1 ==
@ -4601,15 +4624,29 @@ datetimetz_subtract(PyObject *left, PyObject *right)
int offset1, offset2; int offset1, offset2;
PyDateTime_Delta *delta; PyDateTime_Delta *delta;
n1 = classify_utcoffset(left, &offset1); /* Ignore utcoffsets if they have identical tzinfo
assert(n1 != OFFSET_UNKNOWN); * members. This isn't an optimization, it's design.
if (n1 == OFFSET_ERROR) * If utcoffset() doesn't ignore its argument, it may
return NULL; * 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); n2 = classify_utcoffset(right, &offset2);
assert(n2 != OFFSET_UNKNOWN); assert(n2 != OFFSET_UNKNOWN);
if (n2 == OFFSET_ERROR) if (n2 == OFFSET_ERROR)
return NULL; return NULL;
}
if (n1 != n2) { if (n1 != n2) {
PyErr_SetString(PyExc_TypeError, PyErr_SetString(PyExc_TypeError,