[Python-checkins] bpo-32417: Make timedelta arithmetic respect subclasses (#10902)

Alexander Belopolsky webhook-mailer at python.org
Mon Feb 4 14:42:10 EST 2019


https://github.com/python/cpython/commit/89427cd0feae25bbc8693abdccfa6a8c81a2689c
commit: 89427cd0feae25bbc8693abdccfa6a8c81a2689c
branch: master
author: Paul Ganssle <pganssle at users.noreply.github.com>
committer: Alexander Belopolsky <abalkin at users.noreply.github.com>
date: 2019-02-04T14:42:04-05:00
summary:

bpo-32417: Make timedelta arithmetic respect subclasses (#10902)

* Make timedelta return subclass types

Previously timedelta would always return the `date` and `datetime`
types, regardless of what it is added to. This makes it return
an object of the type it was added to.

* Add tests for timedelta arithmetic on subclasses

* Make pure python timedelta return subclass types

* Add test for fromtimestamp with tz argument

* Add tests for subclass behavior in now

* Add news entry.

Fixes:
bpo-32417
bpo-35364

* More descriptive variable names in tests

Addresses Victor's comments

files:
A Misc/NEWS.d/next/Library/2018-12-04-13-35-36.bpo-32417._Y9SKM.rst
M Lib/datetime.py
M Lib/test/datetimetester.py
M Modules/_datetimemodule.c

diff --git a/Lib/datetime.py b/Lib/datetime.py
index 4780b6df8f9b..89c32c0b0a63 100644
--- a/Lib/datetime.py
+++ b/Lib/datetime.py
@@ -1014,7 +1014,7 @@ def __add__(self, other):
         if isinstance(other, timedelta):
             o = self.toordinal() + other.days
             if 0 < o <= _MAXORDINAL:
-                return date.fromordinal(o)
+                return type(self).fromordinal(o)
             raise OverflowError("result out of range")
         return NotImplemented
 
@@ -2024,10 +2024,10 @@ def __add__(self, other):
         hour, rem = divmod(delta.seconds, 3600)
         minute, second = divmod(rem, 60)
         if 0 < delta.days <= _MAXORDINAL:
-            return datetime.combine(date.fromordinal(delta.days),
-                                    time(hour, minute, second,
-                                         delta.microseconds,
-                                         tzinfo=self._tzinfo))
+            return type(self).combine(date.fromordinal(delta.days),
+                                      time(hour, minute, second,
+                                           delta.microseconds,
+                                           tzinfo=self._tzinfo))
         raise OverflowError("result out of range")
 
     __radd__ = __add__
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index d729c7efd52f..958b33675c37 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -820,6 +820,44 @@ def as_hours(self):
         self.assertEqual(str(t3), str(t4))
         self.assertEqual(t4.as_hours(), -1)
 
+    def test_subclass_date(self):
+        class DateSubclass(date):
+            pass
+
+        d1 = DateSubclass(2018, 1, 5)
+        td = timedelta(days=1)
+
+        tests = [
+            ('add', lambda d, t: d + t, DateSubclass(2018, 1, 6)),
+            ('radd', lambda d, t: t + d, DateSubclass(2018, 1, 6)),
+            ('sub', lambda d, t: d - t, DateSubclass(2018, 1, 4)),
+        ]
+
+        for name, func, expected in tests:
+            with self.subTest(name):
+                act = func(d1, td)
+                self.assertEqual(act, expected)
+                self.assertIsInstance(act, DateSubclass)
+
+    def test_subclass_datetime(self):
+        class DateTimeSubclass(datetime):
+            pass
+
+        d1 = DateTimeSubclass(2018, 1, 5, 12, 30)
+        td = timedelta(days=1, minutes=30)
+
+        tests = [
+            ('add', lambda d, t: d + t, DateTimeSubclass(2018, 1, 6, 13)),
+            ('radd', lambda d, t: t + d, DateTimeSubclass(2018, 1, 6, 13)),
+            ('sub', lambda d, t: d - t, DateTimeSubclass(2018, 1, 4, 12)),
+        ]
+
+        for name, func, expected in tests:
+            with self.subTest(name):
+                act = func(d1, td)
+                self.assertEqual(act, expected)
+                self.assertIsInstance(act, DateTimeSubclass)
+
     def test_division(self):
         t = timedelta(hours=1, minutes=24, seconds=19)
         second = timedelta(seconds=1)
@@ -2604,33 +2642,58 @@ def __new__(cls, *args, **kwargs):
         ts = base_d.timestamp()
 
         test_cases = [
-            ('fromtimestamp', (ts,)),
+            ('fromtimestamp', (ts,), base_d),
             # See https://bugs.python.org/issue32417
-            # ('fromtimestamp', (ts, timezone.utc)),
-            ('utcfromtimestamp', (utc_ts,)),
-            ('fromisoformat', (d_isoformat,)),
-            ('strptime', (d_isoformat, '%Y-%m-%dT%H:%M:%S.%f')),
-            ('combine', (date(*args[0:3]), time(*args[3:]))),
+            ('fromtimestamp', (ts, timezone.utc),
+                               base_d.astimezone(timezone.utc)),
+            ('utcfromtimestamp', (utc_ts,), base_d),
+            ('fromisoformat', (d_isoformat,), base_d),
+            ('strptime', (d_isoformat, '%Y-%m-%dT%H:%M:%S.%f'), base_d),
+            ('combine', (date(*args[0:3]), time(*args[3:])), base_d),
         ]
 
-        for constr_name, constr_args in test_cases:
+        for constr_name, constr_args, expected in test_cases:
             for base_obj in (DateTimeSubclass, base_d):
                 # Test both the classmethod and method
                 with self.subTest(base_obj_type=type(base_obj),
                                   constr_name=constr_name):
-                    constr = getattr(base_obj, constr_name)
+                    constructor = getattr(base_obj, constr_name)
 
-                    dt = constr(*constr_args)
+                    dt = constructor(*constr_args)
 
                     # Test that it creates the right subclass
                     self.assertIsInstance(dt, DateTimeSubclass)
 
                     # Test that it's equal to the base object
-                    self.assertEqual(dt, base_d.replace(tzinfo=None))
+                    self.assertEqual(dt, expected)
 
                     # Test that it called the constructor
                     self.assertEqual(dt.extra, 7)
 
+    def test_subclass_now(self):
+        # Test that alternate constructors call the constructor
+        class DateTimeSubclass(self.theclass):
+            def __new__(cls, *args, **kwargs):
+                result = self.theclass.__new__(cls, *args, **kwargs)
+                result.extra = 7
+
+                return result
+
+        test_cases = [
+            ('now', 'now', {}),
+            ('utcnow', 'utcnow', {}),
+            ('now_utc', 'now', {'tz': timezone.utc}),
+            ('now_fixed', 'now', {'tz': timezone(timedelta(hours=-5), "EST")}),
+        ]
+
+        for name, meth_name, kwargs in test_cases:
+            with self.subTest(name):
+                constr = getattr(DateTimeSubclass, meth_name)
+                dt = constr(**kwargs)
+
+                self.assertIsInstance(dt, DateTimeSubclass)
+                self.assertEqual(dt.extra, 7)
+
     def test_fromisoformat_datetime(self):
         # Test that isoformat() is reversible
         base_dates = [
diff --git a/Misc/NEWS.d/next/Library/2018-12-04-13-35-36.bpo-32417._Y9SKM.rst b/Misc/NEWS.d/next/Library/2018-12-04-13-35-36.bpo-32417._Y9SKM.rst
new file mode 100644
index 000000000000..cfc4fbe2e686
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-12-04-13-35-36.bpo-32417._Y9SKM.rst
@@ -0,0 +1,6 @@
+Performing arithmetic between :class:`datetime.datetime` subclasses and
+:class:`datetime.timedelta` now returns an object of the same type as the
+:class:`datetime.datetime` subclass. As a result,
+:meth:`datetime.datetime.astimezone` and alternate constructors like
+:meth:`datetime.datetime.now` and :meth:`datetime.fromtimestamp` called with
+a ``tz`` argument now *also* retain their subclass.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 7997758908bb..c1557b5e6f49 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -3004,7 +3004,8 @@ add_date_timedelta(PyDateTime_Date *date, PyDateTime_Delta *delta, int negate)
     int day = GET_DAY(date) + (negate ? -deltadays : deltadays);
 
     if (normalize_date(&year, &month, &day) >= 0)
-        result = new_date(year, month, day);
+        result = new_date_subclass_ex(year, month, day,
+                                      (PyObject* )Py_TYPE(date));
     return result;
 }
 
@@ -5166,9 +5167,10 @@ add_datetime_timedelta(PyDateTime_DateTime *date, PyDateTime_Delta *delta,
         return NULL;
     }
 
-    return new_datetime(year, month, day,
-                        hour, minute, second, microsecond,
-                        HASTZINFO(date) ? date->tzinfo : Py_None, 0);
+    return new_datetime_subclass_ex(year, month, day,
+                                    hour, minute, second, microsecond,
+                                    HASTZINFO(date) ? date->tzinfo : Py_None,
+                                    (PyObject *)Py_TYPE(date));
 }
 
 static PyObject *



More information about the Python-checkins mailing list