Skip to content

Commit 1d4089b

Browse files
bpo-33812: Corrected astimezone for naive datetimes. (GH-7578) (GH-7601)
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 <abalkin@users.noreply.github.com>
1 parent 040d4a7 commit 1d4089b

5 files changed

Lines changed: 36 additions & 14 deletions

File tree

Doc/library/datetime.rst

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

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

10331032
If called without arguments (or with ``tz=None``) the system local
10341033
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
@@ -1627,14 +1627,17 @@ def astimezone(self, tz=None):
16271627
mytz = self.tzinfo
16281628
if mytz is None:
16291629
mytz = self._local_timezone()
1630+
myoffset = mytz.utcoffset(self)
1631+
else:
1632+
myoffset = mytz.utcoffset(self)
1633+
if myoffset is None:
1634+
mytz = self.replace(tzinfo=None)._local_timezone()
1635+
myoffset = mytz.utcoffset(self)
16301636

16311637
if tz is mytz:
16321638
return self
16331639

16341640
# Convert self to UTC, and attach the new time zone object.
1635-
myoffset = mytz.utcoffset(self)
1636-
if myoffset is None:
1637-
raise ValueError("astimezone() requires an aware datetime")
16381641
utc = (self - myoffset).replace(tzinfo=tz)
16391642

16401643
# 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
@@ -2254,32 +2254,39 @@ def test_replace(self):
22542254
base = cls(2000, 2, 29)
22552255
self.assertRaises(ValueError, base.replace, year=2001)
22562256

2257+
@support.run_with_tz('EDT4')
22572258
def test_astimezone(self):
2258-
return # The rest is no longer applicable
2259-
# Pretty boring! The TZ test is more interesting here. astimezone()
2260-
# simply can't be applied to a naive object.
22612259
dt = self.theclass.now()
2262-
f = FixedOffset(44, "")
2263-
self.assertRaises(ValueError, dt.astimezone) # naive
2260+
f = FixedOffset(44, "0044")
2261+
dt_utc = dt.replace(tzinfo=timezone(timedelta(hours=-4), 'EDT'))
2262+
self.assertEqual(dt.astimezone(), dt_utc) # naive
22642263
self.assertRaises(TypeError, dt.astimezone, f, f) # too many args
22652264
self.assertRaises(TypeError, dt.astimezone, dt) # arg wrong type
2266-
self.assertRaises(ValueError, dt.astimezone, f) # naive
2267-
self.assertRaises(ValueError, dt.astimezone, tz=f) # naive
2265+
dt_f = dt.replace(tzinfo=f) + timedelta(hours=4, minutes=44)
2266+
self.assertEqual(dt.astimezone(f), dt_f) # naive
2267+
self.assertEqual(dt.astimezone(tz=f), dt_f) # naive
22682268

22692269
class Bogus(tzinfo):
22702270
def utcoffset(self, dt): return None
22712271
def dst(self, dt): return timedelta(0)
22722272
bog = Bogus()
22732273
self.assertRaises(ValueError, dt.astimezone, bog) # naive
2274-
self.assertRaises(ValueError,
2275-
dt.replace(tzinfo=bog).astimezone, f)
2274+
self.assertEqual(dt.replace(tzinfo=bog).astimezone(f), dt_f)
22762275

22772276
class AlsoBogus(tzinfo):
22782277
def utcoffset(self, dt): return timedelta(0)
22792278
def dst(self, dt): return None
22802279
alsobog = AlsoBogus()
22812280
self.assertRaises(ValueError, dt.astimezone, alsobog) # also naive
22822281

2282+
class Broken(tzinfo):
2283+
def utcoffset(self, dt): return 1
2284+
def dst(self, dt): return 1
2285+
broken = Broken()
2286+
dt_broken = dt.replace(tzinfo=broken)
2287+
with self.assertRaises(TypeError):
2288+
dt_broken.astimezone()
2289+
22832290
def test_subclass_datetime(self):
22842291

22852292
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
@@ -5201,6 +5201,7 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
52015201
return NULL;
52025202

52035203
if (!HASTZINFO(self) || self->tzinfo == Py_None) {
5204+
naive:
52045205
self_tzinfo = local_timezone_from_local(self);
52055206
if (self_tzinfo == NULL)
52065207
return NULL;
@@ -5221,6 +5222,16 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
52215222
Py_DECREF(self_tzinfo);
52225223
if (offset == NULL)
52235224
return NULL;
5225+
else if(offset == Py_None) {
5226+
Py_DECREF(offset);
5227+
goto naive;
5228+
}
5229+
else if (!PyDelta_Check(offset)) {
5230+
Py_DECREF(offset);
5231+
PyErr_Format(PyExc_TypeError, "utcoffset() returned %.200s,"
5232+
" expected timedelta or None", Py_TYPE(offset)->tp_name);
5233+
return NULL;
5234+
}
52245235
/* result = self - offset */
52255236
result = (PyDateTime_DateTime *)add_datetime_timedelta(self,
52265237
(PyDateTime_Delta *)offset, -1);

0 commit comments

Comments
 (0)