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:
parent
f044e090c4
commit
60c76e4016
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue