From 877b23202b7e7d4f57b58504fd0eb886e8c0b377 Mon Sep 17 00:00:00 2001 From: Alexander Belopolsky Date: Sun, 10 Jun 2018 17:02:58 -0400 Subject: [PATCH] bpo-33812: Corrected astimezone for naive datetimes. (GH-7578) A datetime object d is aware if d.tzinfo is not None and d.tzinfo.utcoffset(d) does not return None. If d.tzinfo is None, or if d.tzinfo is not None but d.tzinfo.utcoffset(d) returns None, d is naive. This commit ensures that instances with non-None d.tzinfo, but d.tzinfo.utcoffset(d) returning None are treated as naive. In addition, C acceleration code will raise TypeError if d.tzinfo.utcoffset(d) returns an object with the type other than timedelta. * Updated the documentation. Assume that the term "naive" is defined elsewhere and remove the not entirely correct clarification. Thanks, Tim. --- Doc/library/datetime.rst | 3 +-- Lib/datetime.py | 9 ++++--- Lib/test/datetimetester.py | 25 ++++++++++++------- .../2018-06-10-13-26-02.bpo-33812.frGAOr.rst | 2 ++ Modules/_datetimemodule.c | 11 ++++++++ 5 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-06-10-13-26-02.bpo-33812.frGAOr.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 8d91f4ef934..1ac2570eae5 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -1058,8 +1058,7 @@ Instance methods: If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its :meth:`utcoffset` and :meth:`dst` methods must not return ``None``. If *self* - is naive (``self.tzinfo is None``), it is presumed to represent time in the - system timezone. + is naive, it is presumed to represent time in the system timezone. If called without arguments (or with ``tz=None``) the system local timezone is assumed for the target timezone. The ``.tzinfo`` attribute of the converted diff --git a/Lib/datetime.py b/Lib/datetime.py index 5e9aab97002..5e922c80b01 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -1773,14 +1773,17 @@ class datetime(date): mytz = self.tzinfo if mytz is None: mytz = self._local_timezone() + myoffset = mytz.utcoffset(self) + else: + myoffset = mytz.utcoffset(self) + if myoffset is None: + mytz = self.replace(tzinfo=None)._local_timezone() + myoffset = mytz.utcoffset(self) if tz is mytz: return self # Convert self to UTC, and attach the new time zone object. - myoffset = mytz.utcoffset(self) - if myoffset is None: - raise ValueError("astimezone() requires an aware datetime") utc = (self - myoffset).replace(tzinfo=tz) # Convert from UTC to tz's local time. diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index a7e5e0b4244..7d4cdac9f41 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2414,25 +2414,24 @@ class TestDateTime(TestDate): base = cls(2000, 2, 29) self.assertRaises(ValueError, base.replace, year=2001) + @support.run_with_tz('EDT4') def test_astimezone(self): - return # The rest is no longer applicable - # Pretty boring! The TZ test is more interesting here. astimezone() - # simply can't be applied to a naive object. dt = self.theclass.now() - f = FixedOffset(44, "") - self.assertRaises(ValueError, dt.astimezone) # naive + f = FixedOffset(44, "0044") + dt_utc = dt.replace(tzinfo=timezone(timedelta(hours=-4), 'EDT')) + self.assertEqual(dt.astimezone(), dt_utc) # naive self.assertRaises(TypeError, dt.astimezone, f, f) # too many args self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type - self.assertRaises(ValueError, dt.astimezone, f) # naive - self.assertRaises(ValueError, dt.astimezone, tz=f) # naive + dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44) + self.assertEqual(dt.astimezone(f), dt_f) # naive + self.assertEqual(dt.astimezone(tz=f), dt_f) # naive class Bogus(tzinfo): def utcoffset(self, dt): return None def dst(self, dt): return timedelta(0) bog = Bogus() self.assertRaises(ValueError, dt.astimezone, bog) # naive - self.assertRaises(ValueError, - dt.replace(tzinfo=bog).astimezone, f) + self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f) class AlsoBogus(tzinfo): def utcoffset(self, dt): return timedelta(0) @@ -2440,6 +2439,14 @@ class TestDateTime(TestDate): alsobog = AlsoBogus() self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive + class Broken(tzinfo): + def utcoffset(self, dt): return 1 + def dst(self, dt): return 1 + broken = Broken() + dt_broken = dt.replace(tzinfo=broken) + with self.assertRaises(TypeError): + dt_broken.astimezone() + def test_subclass_datetime(self): class C(self.theclass): diff --git a/Misc/NEWS.d/next/Library/2018-06-10-13-26-02.bpo-33812.frGAOr.rst b/Misc/NEWS.d/next/Library/2018-06-10-13-26-02.bpo-33812.frGAOr.rst new file mode 100644 index 00000000000..0dc3df6a795 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-06-10-13-26-02.bpo-33812.frGAOr.rst @@ -0,0 +1,2 @@ +Datetime instance d with non-None tzinfo, but with d.tzinfo.utcoffset(d) +returning None is now treated as naive by the astimezone() method. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index cc7eee6fd7f..31aa88d4a26 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -5576,6 +5576,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) return NULL; if (!HASTZINFO(self) || self->tzinfo == Py_None) { + naive: self_tzinfo = local_timezone_from_local(self); if (self_tzinfo == NULL) return NULL; @@ -5596,6 +5597,16 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw) Py_DECREF(self_tzinfo); if (offset == NULL) return NULL; + else if(offset == Py_None) { + Py_DECREF(offset); + goto naive; + } + else if (!PyDelta_Check(offset)) { + Py_DECREF(offset); + PyErr_Format(PyExc_TypeError, "utcoffset() returned %.200s," + " expected timedelta or None", Py_TYPE(offset)->tp_name); + return NULL; + } /* result = self - offset */ result = (PyDateTime_DateTime *)add_datetime_timedelta(self, (PyDateTime_Delta *)offset, -1);