8000 [3.7] bpo-37685: Fixed comparisons of datetime.timedelta and datetime… · python/cpython@6ed20e5 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6ed20e5

Browse files
[3.7] bpo-37685: Fixed comparisons of datetime.timedelta and datetime.timezone. (GH-14996) (GH-15104)
There was a discrepancy between the Python and C implementations. Add singletons ALWAYS_EQ, LARGEST and SMALLEST in test.support to test mixed type comparison. (cherry picked from commit 17e5264)
1 parent 0bb8f22 commit 6ed20e5

File tree

7 files changed

+108
-84
lines changed

7 files changed

+108
-84
lines changed

Doc/library/test.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,28 @@ The :mod:`test.support` module defines the following constants:
356356

357357
Check for presence of docstrings.
358358

359+
359360
.. data:: TEST_HTTP_URL
360361

361362
Define the URL of a dedicated HTTP server for the network tests.
362363

363364

365+
.. data:: ALWAYS_EQ
366+
367+
Object that is equal to anything. Used to test mixed type comparison.
368+
369+
370+
.. data:: LARGEST
371+
372+
Object that is greater than anything (except itself).
373+
Used to test mixed type comparison.
374+
375+
376+
.. data:: SMALLEST
377+
378+
Object that is less than anything (except itself).
379+
Used to test mixed type comparison.
380+
364381

365382
The :mod:`test.support` module defines the following functions:
366383

Lib/datetime.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -724,25 +724,25 @@ def __le__(self, other):
724724
if isinstance(other, timedelta):
725725
return self._cmp(other) <= 0
726726
else:
727-
_cmperror(self, other)
727+
return NotImplemented
728728

729729
def __lt__(self, other):
730730
if isinstance(other, timedelta):
731731
return self._cmp(other) < 0
732732
else:
733-
_cmperror(self, other)
733+
return NotImplemented
734734

735735
def __ge__(self, other):
736736
if isinstance(other, timedelta):
737737
return self._cmp(other) >= 0
738738
else:
739-
_cmperror(self, other)
739+
return NotImplemented
740740

741741
def __gt__(self, other):
742742
if isinstance(other, timedelta):
743743
return self._cmp(other) > 0
744744
else:
745-
_cmperror(self, other)
745+
return NotImplemented
746746

747747
def _cmp(self, other):
748748
assert isinstance(other, timedelta)
@@ -1267,25 +1267,25 @@ def __le__(self, other):
12671267
if isinstance(other, time):
12681268
return self._cmp(other) <= 0
12691269
else:
1270-
_cmperror(self, other)
1270+
return NotImplemented
12711271

12721272
def __lt__(self, other):
12731273
if isinstance(other, time):
12741274
return self._cmp(other) < 0
12751275
else:
1276-
_cmperror(self, other)
1276+
return NotImplemented
12771277

12781278
def __ge__(self, other):
12791279
if isinstance(other, time):
12801280
return self._cmp(other) >= 0
12811281
else:
1282-
_cmperror(self, other)
1282+
return NotImplemented
12831283

12841284
def __gt__(self, other):
12851285
if isinstance(other, time):
12861286
return self._cmp(other) > 0
12871287
else:
1288-
_cmperror(self, other)
1288+
return NotImplemented
12891289

12901290
def _cmp(self, other, allow_mixed=False):
12911291
assert isinstance(other, time)
@@ -2167,9 +2167,9 @@ def __getinitargs__(self):
21672167
return (self._offset, self._name)
21682168

21692169
def __eq__(self, other):
2170-
if type(other) != timezone:
2171-
return False
2172-
return self._offset == other._offset
2170+
if isinstance(other, timezone):
2171+
return self._offset == other._offset
2172+
return NotImplemented
21732173

21742174
def __hash__(self):
21752175
return hash(self._offset)

Lib/test/datetimetester.py

Lines changed: 30 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@
22
33
See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases
44
"""
5-
from test.support import is_resource_enabled
6-
75
import itertools
86
import bisect
9-
107
import copy
118
import decimal
129
import sys
@@ -22,6 +19,7 @@
2219
from operator import lt, le, gt, ge, eq, ne, truediv, floordiv, mod
2320

2421
from test import support
22+
from test.support import is_resource_enabled, ALWAYS_EQ, LARGEST, SMALLEST
2523

2624
import datetime as datetime_module
2725
from datetime import MINYEAR, MAXYEAR
@@ -54,18 +52,6 @@
5452
NAN = float("nan")
5553

5654

57-
class ComparesEqualClass(object):
58-
"""
59-
A class that is always equal to whatever you compare it to.
60-
"""
61-
62-
def __eq__(self, other):
63-
return True
64-
65-
def __ne__(self, other):
66-
return False
67-
68-
6955
#############################################################################
7056
# module tests
7157

@@ -353,6 +339,18 @@ def test_comparison(self):
353339
self.assertTrue(timezone(ZERO) != None)
354340
self.assertFalse(timezone(ZERO) == None)
355341

342+
tz = timezone(ZERO)
343+
self.assertTrue(tz == ALWAYS_EQ)
344+
self.assertFalse(tz != ALWAYS_EQ)
345+
self.assertTrue(tz < LARGEST)
346+
self.assertFalse(tz > LARGEST)
347+
self.assertTrue(tz <= LARGEST)
348+
self.assertFalse(tz >= LARGEST)
349+
self.assertFalse(tz < SMALLEST)
350+
self.assertTrue(tz > SMALLEST)
351+
self.assertFalse(tz <= SMALLEST)
352+
self.assertTrue(tz >= SMALLEST)
353+
356354
def test_aware_datetime(self):
357355
# test that timezone instances can be used by datetime
358356
t = datetime(1, 1, 1)
@@ -414,10 +412,21 @@ def test_harmless_mixed_comparison(self):
414412

415413
# Comparison to objects of unsupported types should return
416414
# NotImplemented which falls back to the right hand side's __eq__
417-
# method. In this case, ComparesEqualClass.__eq__ always returns True.
418-
# ComparesEqualClass.__ne__ always returns False.
419-
self.assertTrue(me == ComparesEqualClass())
420-
self.assertFalse(me != ComparesEqualClass())
415+
# method. In this case, ALWAYS_EQ.__eq__ always returns True.
416+
# ALWAYS_EQ.__ne__ always returns False.
417+
self.assertTrue(me == ALWAYS_EQ)
418+
self.assertFalse(me != ALWAYS_EQ)
419+
420+
# If the other class explicitly defines ordering
421+
# relative to our class, it is allowed to do so
422+
self.assertTrue(me < LARGEST)
423+
self.assertFalse(me > LARGEST)
424+
self.assertTrue(me <= LARGEST)
425+
self.assertFalse(me >= LARGEST)
426+
self.assertFalse(me < SMALLEST)
427+
self.assertTrue(me > SMALLEST)
428+
self.assertFalse(me <= SMALLEST)
429+
self.assertTrue(me >= SMALLEST)
421430

422431
def test_harmful_mixed_comparison(self):
423432
me = self.theclass(1, 1, 1)
@@ -1544,29 +1553,6 @@ class SomeClass:
15441553
self.assertRaises(TypeError, lambda: our < their)
15451554
self.assertRaises(TypeError, lambda: their < our)
15461555

1547-
# However, if the other class explicitly defines ordering
1548-
# relative to our class, it is allowed to do so
1549-
1550-
class LargerThanAnything:
1551-
def __lt__(self, other):
1552-
return False
1553-
def __le__(self, other):
1554-
return isinstance(other, LargerThanAnything)
1555-
def __eq__(self, other):
1556-
return isinstance(other, LargerThanAnything)
1557-
def __gt__(self, other):
1558-
return not isinstance(other, LargerThanAnything)
1559-
def __ge__(self, other):
1560-
return True
1561-
1562-
their = LargerThanAnything()
1563-
self.assertEqual(our == their, False)
1564-
self.assertEqual(their == our, False)
1565-
self.assertEqual(our != their, True)
1566-
self.assertEqual(their != our, True)
1567-
self.assertEqual(our < their, True)
1568-
self.assertEqual(their < our, False)
1569-
15701556
def test_bool(self):
15711557
# All dates are considered true.
15721558
self.assertTrue(self.theclass.min)
@@ -3642,8 +3628,8 @@ def test_replace(self):
36423628
self.assertRaises(ValueError, base.replace, microsecond=1000000)
36433629

36443630
def test_mixed_compare(self):
3645-
t1 = time(1, 2, 3)
3646-
t2 = time(1, 2, 3)
3631+
t1 = self.theclass(1, 2, 3)
3632+
t2 = self.theclass(1, 2, 3)
36473633
self.assertEqual(t1, t2)
36483634
t2 = t2.replace(tzinfo=None)
36493635
self.assertEqual(t1, t2)

Lib/test/support/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"run_with_locale", "swap_item",
110110
"swap_attr", "Matcher", "set_memlimit", "SuppressCrashReport", "sortdict",
111111
"run_with_tz", "PGO", "missing_compiler_executable", "fd_count",
112+
"ALWAYS_EQ", "LARGEST", "SMALLEST"
112113
]
113114

114115
class Error(Exception):
@@ -2919,3 +2920,39 @@ def __fspath__(self):
29192920
raise self.path
29202921
else:
29212922
return self.path
2923+
2924+
2925+
class _ALWAYS_EQ:
2926+
"""
2927+
Object that is equal to anything.
2928+
"""
2929+
def __eq__(self, other):
2930+
return True
2931+
def __ne__(self, other):
2932+
return False
2933+
2934+
ALWAYS_EQ = _ALWAYS_EQ()
2935+
2936+
@functools.total_ordering
2937+
class _LARGEST:
2938+
"""
2939+
Object that is greater than anything (except itself).
2940+
"""
2941+
def __eq__(self, other):
2942+
return isinstance(other, _LARGEST)
2943+
def __lt__(self, other):
2944+
return False
2945+
2946+
LARGEST = _LARGEST()
2947+
2948+
@functools.total_ordering
2949+
class _SMALLEST:
2950+
"""
2951+
Object that is less than anything (except itself).
2952+
"""
2953+
def __eq__(self, other):
2954+
return isinstance(other, _SMALLEST)
2955+
def __gt__(self, other):
2956+
return False
2957+
2958+
SMALLEST = _SMALLEST()

Lib/test/test_ipaddress.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import pickle
1313
import ipaddress
1414
import weakref
15+
from test.support import LARGEST, SMALLEST
1516

1617

1718
class BaseTestCase(unittest.TestCase):
@@ -679,20 +680,6 @@ def test_ip_network(self):
679680
self.assertFactoryError(ipaddress.ip_network, "network")
680681

681682

682-
@functools.total_ordering
683-
class LargestObject:
684-
def __eq__(self, other):
685-
return isinstance(other, LargestObject)
686-
def __lt__(self, other):
687-
return False
688-
689-
@functools.total_ordering
690-
class SmallestObject:
691-
def __eq__(self, other):
692-
return isinstance(other, SmallestObject)
693-
def __gt__(self, other):
694-
return False
695-
696683
class ComparisonTests(unittest.TestCase):
697684

698685
v4addr = ipaddress.IPv4Address(1)
@@ -781,8 +768,6 @@ def test_mixed_type_ordering(self):
781768

782769
def test_foreign_type_ordering(self):
783770
other = object()
784-
smallest = SmallestObject()
785-
largest = LargestObject()
786771
for obj in self.objects:
787772
with self.assertRaises(TypeError):
788773
obj < other
@@ -792,14 +777,14 @@ def test_foreign_type_ordering(self):
792777
obj <= other
793778
with self.assertRaises(TypeError):
794779
obj >= other
795-
self.assertTrue(obj < largest)
796-
self.assertFalse(obj > largest)
797-
self.assertTrue(obj <= largest)
798-
self.assertFalse(obj >= largest)
799-
self.assertFalse(obj < smallest)
800-
self.assertTrue(obj > smallest)
801-
self.assertFalse(obj <= smallest)
802-
self.assertTrue(obj >= smallest)
780+
self.assertTrue(obj < LARGEST)
781+
self.assertFalse(obj > LARGEST)
782+
self.assertTrue(obj <= LARGEST)
783+
self.assertFalse(obj >= LARGEST)
784+
self.assertFalse(obj < SMALLEST)
785+
self.assertTrue(obj > SMALLEST)
786+
self.assertFalse(obj <= SMALLEST)
787+
self.assertTrue(obj >= SMALLEST)
803788

804789
def test_mixed_type_key(self):
805790
# with get_mixed_type_key, you can sort addresses and network.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed comparisons of :class:`datetime.timedelta` and
2+
:class:`datetime.timezone`.

Modules/_datetimemodule.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3644,11 +3644,8 @@ timezone_richcompare(PyDateTime_TimeZone *self,
36443644
{
36453645
if (op != Py_EQ && op != Py_NE)
36463646
Py_RETURN_NOTIMPLEMENTED;
3647-
if (Py_TYPE(other) != &PyDateTime_TimeZoneType) {
3648-
if (op == Py_EQ)
3649-
Py_RETURN_FALSE;
3650-
else
3651-
Py_RETURN_TRUE;
3647+
if (!PyTZInfo_Check(other)) {
3648+
Py_RETURN_NOTIMPLEMENTED;
36523649
}
36533650
return delta_richcompare(self->offset, other->offset, op);
36543651
}

0 commit comments

Comments
 (0)
0