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.
This commit is contained in:
Alexander Belopolsky 2018-06-10 17:02:58 -04:00 committed by GitHub
parent af4b0130d4
commit 877b23202b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 36 additions and 14 deletions

View File

@ -1058,8 +1058,7 @@ Instance methods:
If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its 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* :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 is naive, it is presumed to represent time in the system timezone.
system timezone.
If called without arguments (or with ``tz=None``) the system local If called without arguments (or with ``tz=None``) the system local
timezone is assumed for the target timezone. The ``.tzinfo`` attribute of the converted timezone is assumed for the target timezone. The ``.tzinfo`` attribute of the converted

View File

@ -1773,14 +1773,17 @@ class datetime(date):
mytz = self.tzinfo mytz = self.tzinfo
if mytz is None: if mytz is None:
mytz = self._local_timezone() 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: if tz is mytz:
return self return self
# Convert self to UTC, and attach the new time zone object. # 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) utc = (self - myoffset).replace(tzinfo=tz)
# Convert from UTC to tz's local time. # Convert from UTC to tz's local time.

View File

@ -2414,25 +2414,24 @@ class TestDateTime(TestDate):
base = cls(2000, 2, 29) base = cls(2000, 2, 29)
self.assertRaises(ValueError, base.replace, year=2001) self.assertRaises(ValueError, base.replace, year=2001)
@support.run_with_tz('EDT4')
def test_astimezone(self): 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() dt = self.theclass.now()
f = FixedOffset(44, "") f = FixedOffset(44, "0044")
self.assertRaises(ValueError, dt.astimezone) # naive 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, f, f) # too many args
self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
self.assertRaises(ValueError, dt.astimezone, f) # naive dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44)
self.assertRaises(ValueError, dt.astimezone, tz=f) # naive self.assertEqual(dt.astimezone(f), dt_f) # naive
self.assertEqual(dt.astimezone(tz=f), dt_f) # naive
class Bogus(tzinfo): class Bogus(tzinfo):
def utcoffset(self, dt): return None def utcoffset(self, dt): return None
def dst(self, dt): return timedelta(0) def dst(self, dt): return timedelta(0)
bog = Bogus() bog = Bogus()
self.assertRaises(ValueError, dt.astimezone, bog) # naive self.assertRaises(ValueError, dt.astimezone, bog) # naive
self.assertRaises(ValueError, self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f)
dt.replace(tzinfo=bog).astimezone, f)
class AlsoBogus(tzinfo): class AlsoBogus(tzinfo):
def utcoffset(self, dt): return timedelta(0) def utcoffset(self, dt): return timedelta(0)
@ -2440,6 +2439,14 @@ class TestDateTime(TestDate):
alsobog = AlsoBogus() alsobog = AlsoBogus()
self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive 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): def test_subclass_datetime(self):
class C(self.theclass): class C(self.theclass):

View File

@ -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.

View File

@ -5576,6 +5576,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
return NULL; return NULL;
if (!HASTZINFO(self) || self->tzinfo == Py_None) { if (!HASTZINFO(self) || self->tzinfo == Py_None) {
naive:
self_tzinfo = local_timezone_from_local(self); self_tzinfo = local_timezone_from_local(self);
if (self_tzinfo == NULL) if (self_tzinfo == NULL)
return NULL; return NULL;
@ -5596,6 +5597,16 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
Py_DECREF(self_tzinfo); Py_DECREF(self_tzinfo);
if (offset == NULL) if (offset == NULL)
return 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 = self - offset */
result = (PyDateTime_DateTime *)add_datetime_timedelta(self, result = (PyDateTime_DateTime *)add_datetime_timedelta(self,
(PyDateTime_Delta *)offset, -1); (PyDateTime_Delta *)offset, -1);