Skip to content

Commit b8687df

Browse files
committed
#665194: Update email.utils.localtime to use astimezone, and fix bug.
The new code correctly handles historic changes in UTC offsets. A test for this should follow. Original patch by Alexander Belopolsky.
1 parent 17183a2 commit b8687df

File tree

3 files changed

+33
-31
lines changed

3 files changed

+33
-31
lines changed

Lib/email/utils.py

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -386,33 +386,26 @@ def localtime(dt=None, isdst=-1):
386386
387387
"""
388388
if dt is None:
389-
seconds = time.time()
390-
else:
391-
if dt.tzinfo is None:
392-
# A naive datetime is given. Convert to a (localtime)
393-
# timetuple and pass to system mktime together with
394-
# the isdst hint. System mktime will return seconds
395-
# sysce epoch.
396-
tm = dt.timetuple()[:-1] + (isdst,)
397-
seconds = time.mktime(tm)
389+
dt = datetime.datetime.now(datetime.timezone.utc)
390+
if dt.tzinfo is not None:
391+
return dt.astimezone()
392+
# We have a naive datetime. Convert to a (localtime) timetuple and pass to
393+
# system mktime together with the isdst hint. System mktime will return
394+
# seconds since epoch.
395+
tm = dt.timetuple()[:-1] + (isdst,)
396+
seconds = time.mktime(tm)
397+
localtm = time.localtime(seconds)
398+
try:
399+
delta = datetime.timedelta(seconds=localtm.tm_gmtoff)
400+
tz = datetime.timezone(delta, localtm.tm_zone)
401+
except AttributeError:
402+
# Compute UTC offset and compare with the value implied by tm_isdst.
403+
# If the values match, use the zone name implied by tm_isdst.
404+
delta = dt - datetime.datetime(*time.gmtime(ts)[:6])
405+
dst = time.daylight and localtm.tm_isdst > 0
406+
gmtoff = -(time.altzone if dst else time.timezone)
407+
if delta == datetime.timedelta(seconds=gmtoff):
408+
tz = datetime.timezone(delta, time.tzname[dst])
398409
else:
399-
# An aware datetime is given. Use aware datetime
400-
# arithmetics to find seconds since epoch.
401-
delta = dt - datetime.datetime(1970, 1, 1,
402-
tzinfo=datetime.timezone.utc)
403-
seconds = delta.total_seconds()
404-
tm = time.localtime(seconds)
405-
406-
# XXX: The following logic may not work correctly if UTC
407-
# offset has changed since time provided in dt. This will be
408-
# corrected in C implementation for platforms that support
409-
# tm_gmtoff.
410-
if time.daylight and tm.tm_isdst:
411-
offset = time.altzone
412-
tzname = time.tzname[1]
413-
else:
414-
offset = time.timezone
415-
tzname = time.tzname[0]
416-
417-
tz = datetime.timezone(datetime.timedelta(seconds=-offset), tzname)
418-
return datetime.datetime.fromtimestamp(seconds, tz)
410+
tz = datetime.timezone(delta)
411+
return dt.replace(tzinfo=tz)

Lib/test/test_email/test_utils.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,23 @@ def test_localtime_daylight_false_dst_true(self):
8787
t2 = utils.localtime(t1)
8888
self.assertEqual(t1, t2)
8989

90+
@test.support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
9091
def test_localtime_epoch_utc_daylight_true(self):
9192
test.support.patch(self, time, 'daylight', True)
9293
t0 = datetime.datetime(1970, 1, 1, tzinfo = datetime.timezone.utc)
9394
t1 = utils.localtime(t0)
94-
self.assertEqual(t0, t1)
95+
t2 = t0 - datetime.timedelta(hours=5)
96+
t2 = t2.replace(tzinfo = datetime.timezone(datetime.timedelta(hours=-5)))
97+
self.assertEqual(t1, t2)
9598

99+
@test.support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
96100
def test_localtime_epoch_utc_daylight_false(self):
97101
test.support.patch(self, time, 'daylight', False)
98102
t0 = datetime.datetime(1970, 1, 1, tzinfo = datetime.timezone.utc)
99103
t1 = utils.localtime(t0)
100-
self.assertEqual(t0, t1)
104+
t2 = t0 - datetime.timedelta(hours=5)
105+
t2 = t2.replace(tzinfo = datetime.timezone(datetime.timedelta(hours=-5)))
106+
self.assertEqual(t1, t2)
101107

102108
def test_localtime_epoch_notz_daylight_true(self):
103109
test.support.patch(self, time, 'daylight', True)

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ Core and Builtins
2424
Library
2525
-------
2626

27+
- Issue ##665194: Update email.utils.localtime to use datetime.astimezone and
28+
correctly handle historic changes in UTC offsets.
29+
2730
- Issue #15199: Fix JavaScript's default MIME type to application/javascript.
2831
Patch by Bohuslav Kabrda.
2932

0 commit comments

Comments
 (0)