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:
parent
af4b0130d4
commit
877b23202b
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in New Issue