Skip to content

Commit 037e912

Browse files
bpo-33812: Corrected astimezone for naive datetimes. (GH-7578) (GH-7600)
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. (cherry picked from commit 877b232) Co-authored-by: Alexander Belopolsky <[email protected]>
1 parent 298eb07 commit 037e912

File tree

5 files changed

+36
-14
lines changed

5 files changed

+36
-14
lines changed

Doc/library/datetime.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,8 +1058,7 @@ Instance methods:
10581058

10591059
If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its
10601060
:meth:`utcoffset` and :meth:`dst` methods must not return ``None``. If *self*
1061-
is naive (``self.tzinfo is None``), it is presumed to represent time in the
1062-
system timezone.
1061+
is naive, it is presumed to represent time in the system timezone.
10631062

10641063
If called without arguments (or with ``tz=None``) the system local
10651064
timezone is assumed for the target timezone. The ``.tzinfo`` attribute of the converted

Lib/datetime.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,14 +1780,17 @@ def astimezone(self, tz=None):
17801780
mytz = self.tzinfo
17811781
if mytz is None:
17821782
mytz = self._local_timezone()
1783+
myoffset = mytz.utcoffset(self)
1784+
else:
1785+
myoffset = mytz.utcoffset(self)
1786+
if myoffset is None:
1787+
mytz = self.replace(tzinfo=None)._local_timezone()
1788+
myoffset = mytz.utcoffset(self)
17831789

17841790
if tz is mytz:
17851791
return self
17861792

17871793
# Convert self to UTC, and attach the new time zone object.
1788-
myoffset = mytz.utcoffset(self)
1789-
if myoffset is None:
1790-
raise ValueError("astimezone() requires an aware datetime")
17911794
utc = (self - myoffset).replace(tzinfo=tz)
17921795

17931796
# Convert from UTC to tz's local time.

Lib/test/datetimetester.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,32 +2414,39 @@ def test_replace(self):
24142414
base = cls(2000, 2, 29)
24152415
self.assertRaises(ValueError, base.replace, year=2001)
24162416

2417+
@support.run_with_tz('EDT4')
24172418
def test_astimezone(self):
2418-
return # The rest is no longer applicable
2419-
# Pretty boring! The TZ test is more interesting here. astimezone()
2420-
# simply can't be applied to a naive object.
24212419
dt = self.theclass.now()
2422-
f = FixedOffset(44, "")
2423-
self.assertRaises(ValueError, dt.astimezone) # naive
2420+
f = FixedOffset(44, "0044")
2421+
dt_utc = dt.replace(tzinfo=timezone(timedelta(hours=-4), 'EDT'))
2422+
self.assertEqual(dt.astimezone(), dt_utc) # naive
24242423
self.assertRaises(TypeError, dt.astimezone, f, f) # too many args
24252424
self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
2426-
self.assertRaises(ValueError, dt.astimezone, f) # naive
2427-
self.assertRaises(ValueError, dt.astimezone, tz=f) # naive
2425+
dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44)
2426+
self.assertEqual(dt.astimezone(f), dt_f) # naive
2427+
self.assertEqual(dt.astimezone(tz=f), dt_f) # naive
24282428

24292429
class Bogus(tzinfo):
24302430
def utcoffset(self, dt): return None
24312431
def dst(self, dt): return timedelta(0)
24322432
bog = Bogus()
24332433
self.assertRaises(ValueError, dt.astimezone, bog) # naive
2434-
self.assertRaises(ValueError,
2435-
dt.replace(tzinfo=bog).astimezone, f)
2434+
self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f)
24362435

24372436
class AlsoBogus(tzinfo):
24382437
def utcoffset(self, dt): return timedelta(0)
24392438
def dst(self, dt): return None
24402439
alsobog = AlsoBogus()
24412440
self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive
24422441

2442+
class Broken(tzinfo):
2443+
def utcoffset(self, dt): return 1
2444+
def dst(self, dt): return 1
2445+
broken = Broken()
2446+
dt_broken = dt.replace(tzinfo=broken)
2447+
with self.assertRaises(TypeError):
2448+
dt_broken.astimezone()
2449+
24432450
def test_subclass_datetime(self):
24442451

24452452
class C(self.theclass):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Datetime instance d with non-None tzinfo, but with d.tzinfo.utcoffset(d)
2+
returning None is now treated as naive by the astimezone() method.

Modules/_datetimemodule.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5576,6 +5576,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
55765576
return NULL;
55775577

55785578
if (!HASTZINFO(self) || self->tzinfo == Py_None) {
5579+
naive:
55795580
self_tzinfo = local_timezone_from_local(self);
55805581
if (self_tzinfo == NULL)
55815582
return NULL;
@@ -5596,6 +5597,16 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
55965597
Py_DECREF(self_tzinfo);
55975598
if (offset == NULL)
55985599
return NULL;
5600+
else if(offset == Py_None) {
5601+
Py_DECREF(offset);
5602+
goto naive;
5603+
}
5604+
else if (!PyDelta_Check(offset)) {
5605+
Py_DECREF(offset);
5606+
PyErr_Format(PyExc_TypeError, "utcoffset() returned %.200s,"
5607+
" expected timedelta or None", Py_TYPE(offset)->tp_name);
5608+
return NULL;
5609+
}
55995610
/* result = self - offset */
56005611
result = (PyDateTime_DateTime *)add_datetime_timedelta(self,
56015612
(PyDateTime_Delta *)offset, -1);

0 commit comments

Comments
 (0)