Skip to content

Commit a612d89

Browse files
committed
Address review comments for libc timezone globals
Rewrite test_strptime.test_timezone to check each tzname entry and treat UTC/GMT as standard time, so zones with negative DST such as Europe/Dublin pass. Add a round-trip test with fixed EST5EDT rules. Document the glibc tzset() early-return caveat in init_timezone(). In get_dst_offset(), skip a normalized sample date instead of abandoning the remaining months, and drop a dead tm_mon range check. Expand the NEWS entry to cover the negative DST classification change, daylight being nonzero for historical DST, and altzone sampling on glibc and Windows.
1 parent 7fad7cb commit a612d89

3 files changed

Lines changed: 45 additions & 18 deletions

File tree

‎Lib/test/test_strptime.py‎

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -474,18 +474,35 @@ def test_timezone(self):
474474
self.assertEqual(strp_output.tm_isdst, 0)
475475
strp_output = _strptime._strptime_time("GMT", "%Z")
476476
self.assertEqual(strp_output.tm_isdst, 0)
477-
time_tuple = time.localtime()
478-
strf_output = time.strftime("%Z") #UTC does not have a timezone
479-
strp_output = _strptime._strptime_time(strf_output, "%Z")
480-
locale_time = _strptime.LocaleTime()
481-
if time.tzname[0] != time.tzname[1] or not time.daylight:
482-
self.assertTrue(strp_output[8] == time_tuple[8],
483-
"timezone check failed; '%s' -> %s != %s" %
484-
(strf_output, strp_output[8], time_tuple[8]))
485-
else:
486-
self.assertTrue(strp_output[8] == -1,
487-
"LocaleTime().timezone has duplicate values and "
488-
"time.daylight but timezone value not set to -1")
477+
# %Z recognizes the module's timezone names. UTC and GMT are always
478+
# parsed as standard time, even in zones such as Europe/Dublin where
479+
# libc reports GMT as (negative) DST.
480+
for isdst, name in enumerate(time.tzname):
481+
if not name or (isdst and not time.daylight):
482+
continue
483+
with self.subTest(name=name, isdst=isdst):
484+
if name.lower() in ('utc', 'gmt'):
485+
expected = 0
486+
elif time.daylight and time.tzname[0] == time.tzname[1]:
487+
expected = -1
488+
else:
489+
expected = isdst
490+
parsed = _strptime._strptime_time(name, '%Z')
491+
self.assertEqual(parsed.tm_isdst, expected)
492+
493+
@support.run_with_tz('EST5EDT,M3.2.0,M11.1.0')
494+
@skip_if_buggy_ucrt_strfptime
495+
def test_timezone_roundtrip(self):
496+
# Use stable, explicit rules when comparing strftime(), strptime(),
497+
# and localtime(), including both standard time and DST.
498+
for month, isdst in ((1, 0), (7, 1)):
499+
with self.subTest(month=month):
500+
t = time.mktime((2000, month, 1, 12, 0, 0, 0, 0, -1))
501+
local = time.localtime(t)
502+
self.assertEqual(local.tm_isdst, isdst)
503+
name = time.strftime('%Z', local)
504+
parsed = _strptime._strptime_time(name, '%Z')
505+
self.assertEqual(parsed.tm_isdst, isdst)
489506

490507
@unittest.skipUnless(
491508
hasattr(time, "tzset"), "time module has no attribute tzset"

‎Misc/NEWS.d/next/Library/2026-11-04-12-13-21.gh-issue-161194.GiNgY1.rst‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,12 @@ On platforms where libc provides the ``timezone``, ``daylight`` and
33
:data:`time.tzname` are now taken from libc instead of being guessed from
44
samples of the local time in January and July. This fixes stale values for
55
zones whose rules change during the year, such as ``America/Vancouver`` in
6-
2026. :data:`time.altzone` now equals :data:`time.timezone` for zones
7-
without DST when libc does not provide ``altzone``.
6+
2026. Standard and DST are now classified by libc rather than by comparing
7+
offsets, so for zones with negative DST such as ``Europe/Dublin``,
8+
:data:`time.tzname` is now ``('IST', 'GMT')`` and :data:`time.timezone` is
9+
``-3600``, matching :attr:`~time.struct_time.tm_isdst`. On some platforms
10+
:data:`time.daylight` may now be nonzero for zones that observed DST only in
11+
the past. When libc does not provide ``altzone`` (including glibc and
12+
Windows), :data:`time.altzone` is now found by sampling the current year for
13+
a DST offset instead of assuming a one-hour shift, and equals
14+
:data:`time.timezone` for zones without DST.

‎Modules/timemodule.c‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,9 +1898,6 @@ get_dst_offset(time_t now, long dst_offset)
18981898
PyErr_Clear();
18991899
return dst_offset;
19001900
}
1901-
if (current.tm_mon < 0 || current.tm_mon > 11) {
1902-
return dst_offset;
1903-
}
19041901

19051902
int found_dst = 0;
19061903
for (int month = 0; month < 12; month++) {
@@ -1933,7 +1930,7 @@ get_dst_offset(time_t now, long dst_offset)
19331930
|| (month < current.tm_mon && sample > now)
19341931
|| (month > current.tm_mon && sample < now))
19351932
{
1936-
break;
1933+
continue;
19371934
}
19381935
}
19391936
if (p.tm_isdst > 0) {
@@ -1983,6 +1980,12 @@ init_timezone(PyObject *m)
19831980
#ifdef _Py_HAVE_TIMEZONE_GLOBALS
19841981
PyObject *otz0, *otz1;
19851982
#if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
1983+
/* glibc caveat: tzset() returns early if TZ is unchanged since the
1984+
last call, and localtime() updates tzname and daylight for the rules
1985+
in effect at the converted time. So when time.tzset() is called
1986+
without changing TZ, the values read here reflect the most recent
1987+
localtime() call rather than a fresh read of the zone data. There
1988+
is no portable way to force a re-read. */
19861989
tzset();
19871990
#endif
19881991
ADD_INT("timezone", _Py_timezone);

0 commit comments

Comments
 (0)