Skip to content

Commit 52390a4

Browse files
committed
gh-161194: Use libc timezone globals when available
1 parent 5346059 commit 52390a4

9 files changed

Lines changed: 132 additions & 28 deletions

File tree

‎Doc/library/time.rst‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,9 @@ Timezone Constants
10261026

10271027
The offset of the local DST timezone, in seconds west of UTC, if one is defined.
10281028
This is negative if the local DST timezone is east of UTC (as in Western Europe,
1029-
including the UK). Only use this if ``daylight`` is nonzero. See note below.
1029+
including the UK). Only use this if ``daylight`` is nonzero. On some
1030+
platforms, this is approximated as :data:`timezone` minus 3600, which may
1031+
be incorrect for timezones with a different DST shift. See note below.
10301032

10311033
.. data:: daylight
10321034

‎Lib/test/test_strptime.py‎

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -474,18 +474,34 @@ 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, which need not match
478+
# localtime() for every date in a zone whose rules have changed.
479+
for isdst, name in enumerate(time.tzname):
480+
if not name or (isdst and not time.daylight):
481+
continue
482+
with self.subTest(name=name, isdst=isdst):
483+
if name.lower() in ('utc', 'gmt'):
484+
expected = 0
485+
elif time.daylight and time.tzname[0] == time.tzname[1]:
486+
expected = -1
487+
else:
488+
expected = isdst
489+
parsed = _strptime._strptime_time(name, '%Z')
490+
self.assertEqual(parsed.tm_isdst, expected)
491+
492+
@support.run_with_tz('EST5EDT,M3.2.0,M11.1.0')
493+
@skip_if_buggy_ucrt_strfptime
494+
def test_timezone_roundtrip(self):
495+
# Use stable, explicit rules when comparing strftime(), strptime(),
496+
# and localtime(), including both standard time and DST.
497+
for month, isdst in ((1, 0), (7, 1)):
498+
with self.subTest(month=month):
499+
t = time.mktime((2000, month, 1, 12, 0, 0, 0, 0, -1))
500+
local = time.localtime(t)
501+
self.assertEqual(local.tm_isdst, isdst)
502+
name = time.strftime('%Z', local)
503+
parsed = _strptime._strptime_time(name, '%Z')
504+
self.assertEqual(parsed.tm_isdst, isdst)
489505

490506
@unittest.skipUnless(
491507
hasattr(time, "tzset"), "time module has no attribute tzset"

‎Lib/test/test_time.py‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,16 @@ def test_tzset(self):
561561
del environ['TZ']
562562
time.tzset()
563563

564+
@unittest.skipUnless(sysconfig.get_config_var('_Py_HAVE_TIMEZONE_GLOBALS'),
565+
'requires libc timezone globals')
566+
@support.run_with_tz('IST-1GMT0,M10.5.0,M3.5.0/1')
567+
def test_tzset_negative_dst(self):
568+
# Prefer libc's standard/DST classification over ordering offsets.
569+
# altzone may still assume a one-hour positive DST shift.
570+
self.assertEqual(time.timezone, -3600)
571+
self.assertEqual(time.tzname, ('IST', 'GMT'))
572+
self.assertEqual(time.daylight, 1)
573+
564574
def test_insane_timestamps(self):
565575
# It's possible that some platform maps time_t to double,
566576
# and that this test will fail there. This test should
@@ -858,17 +868,10 @@ def test_localtime_timezone(self):
858868
lt = time.localtime()
859869
self.assertHasAttr(lt, "tm_gmtoff")
860870
self.assertHasAttr(lt, "tm_zone")
861-
862-
# See if the offset and zone are similar to the module
863-
# attributes.
864871
if lt.tm_gmtoff is None:
865872
self.assertNotHasAttr(time, "timezone")
866-
else:
867-
self.assertEqual(lt.tm_gmtoff, -[time.timezone, time.altzone][lt.tm_isdst])
868873
if lt.tm_zone is None:
869874
self.assertNotHasAttr(time, "tzname")
870-
else:
871-
self.assertEqual(lt.tm_zone, time.tzname[lt.tm_isdst])
872875

873876
# Try and make UNIX times from the localtime and a 9-tuple
874877
# created from the localtime. Test to see that the times are
@@ -886,6 +889,21 @@ def test_localtime_timezone(self):
886889
self.assertEqual(new_lt.tm_gmtoff, lt.tm_gmtoff)
887890
self.assertEqual(new_lt9.tm_zone, lt.tm_zone)
888891

892+
@support.run_with_tz('EST5EDT,M3.2.0,M11.1.0')
893+
@skip_if_buggy_ucrt_strfptime
894+
def test_localtime_timezone_constants(self):
895+
# The module constants cannot describe every historical or current
896+
# offset in zones whose rules have changed. Compare them against
897+
# localtime() using explicit rules with a one-hour DST shift.
898+
for month, isdst in ((1, 0), (7, 1)):
899+
with self.subTest(month=month):
900+
t = time.mktime((2000, month, 1, 12, 0, 0, 0, 0, -1))
901+
lt = time.localtime(t)
902+
self.assertEqual(lt.tm_isdst, isdst)
903+
self.assertEqual(lt.tm_gmtoff,
904+
-(time.timezone, time.altzone)[isdst])
905+
self.assertEqual(lt.tm_zone, time.tzname[isdst])
906+
889907
def test_strptime_timezone(self):
890908
t = time.strptime("UTC", "%Z")
891909
self.assertEqual(t.tm_zone, 'UTC')
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Use libc's ``timezone``, ``daylight`` and ``tzname`` variables for the
2+
:mod:`time` module constants when available, rather than estimating them
3+
from January and July samples. This fixes stale constants after timezone
4+
rule changes and the standard/DST classification for negative DST zones.
5+
The existing one-hour approximation for :data:`time.altzone` is unchanged
6+
on platforms without a native ``altzone`` variable.

‎Modules/timemodule.c‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1854,7 +1854,7 @@ time_get_clock_info_impl(PyObject *module, const char *name)
18541854
return NULL;
18551855
}
18561856

1857-
#ifndef HAVE_DECL_TZNAME
1857+
#ifndef _Py_HAVE_TIMEZONE_GLOBALS
18581858
static void
18591859
get_zone(char *zone, int n, struct tm *p)
18601860
{
@@ -1875,7 +1875,7 @@ get_gmtoff(time_t t, struct tm *p)
18751875
return timegm(p) - t;
18761876
#endif
18771877
}
1878-
#endif // !HAVE_DECL_TZNAME
1878+
#endif // !_Py_HAVE_TIMEZONE_GLOBALS
18791879

18801880
static int
18811881
init_timezone(PyObject *m)
@@ -1904,7 +1904,7 @@ init_timezone(PyObject *m)
19041904
19051905
And I'm lazy and hate C so nyer.
19061906
*/
1907-
#ifdef HAVE_DECL_TZNAME
1907+
#ifdef _Py_HAVE_TIMEZONE_GLOBALS
19081908
PyObject *otz0, *otz1;
19091909
#if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
19101910
tzset();
@@ -1913,6 +1913,7 @@ init_timezone(PyObject *m)
19131913
#ifdef HAVE_ALTZONE
19141914
ADD_INT("altzone", altzone);
19151915
#else
1916+
/* Assume a one-hour DST shift when libc does not provide altzone. */
19161917
ADD_INT("altzone", _Py_timezone-3600);
19171918
#endif
19181919
ADD_INT("daylight", _Py_daylight);
@@ -1942,7 +1943,7 @@ init_timezone(PyObject *m)
19421943
if (PyModule_Add(m, "tzname", Py_BuildValue("(NN)", otz0, otz1)) < 0) {
19431944
return -1;
19441945
}
1945-
#else // !HAVE_DECL_TZNAME
1946+
#else // !_Py_HAVE_TIMEZONE_GLOBALS
19461947
static const time_t YEAR = (365 * 24 + 6) * 3600;
19471948
time_t t;
19481949
struct tm p;
@@ -1987,7 +1988,7 @@ init_timezone(PyObject *m)
19871988
if (PyModule_Add(m, "tzname", tzname_obj) < 0) {
19881989
return -1;
19891990
}
1990-
#endif // !HAVE_DECL_TZNAME
1991+
#endif // !_Py_HAVE_TIMEZONE_GLOBALS
19911992
#undef ADD_INT
19921993

19931994
if (PyErr_Occurred()) {

‎PC/pyconfig.h‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,9 +429,8 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */
429429
/* Define to 1 if you have the <direct.h> header file. */
430430
#define HAVE_DIRECT_H 1
431431

432-
/* Define to 1 if you have the declaration of `tzname', and to 0 if you don't.
433-
*/
434-
#define HAVE_DECL_TZNAME 1
432+
/* Define if time.h declares the timezone, daylight and tzname variables. */
433+
#define _Py_HAVE_TIMEZONE_GLOBALS 1
435434

436435
/* Define if you have dirent.h. */
437436
/* #define DIRENT 1 */

‎configure‎

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎configure.ac‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6480,6 +6480,22 @@ if test $ac_cv_header_time_altzone = yes; then
64806480
[Define this if your time.h defines altzone.])
64816481
fi
64826482

6483+
AC_CACHE_CHECK([for time.h that defines timezone, daylight and tzname variables],
6484+
[ac_cv_header_time_tz_globals], [
6485+
AC_LINK_IFELSE([AC_LANG_PROGRAM([[@%:@include <time.h>]], [[
6486+
long tz = timezone;
6487+
int dl = daylight;
6488+
char *name = tzname[0];
6489+
return (int)tz + dl + (name != 0);
6490+
]])],
6491+
[ac_cv_header_time_tz_globals=yes],
6492+
[ac_cv_header_time_tz_globals=no])
6493+
])
6494+
if test $ac_cv_header_time_tz_globals = yes; then
6495+
AC_DEFINE([_Py_HAVE_TIMEZONE_GLOBALS], [1],
6496+
[Define if time.h declares the timezone, daylight and tzname variables.])
6497+
fi
6498+
64836499
AC_CACHE_CHECK([for addrinfo], [ac_cv_struct_addrinfo],
64846500
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[@%:@include <netdb.h>]], [[struct addrinfo a]])],
64856501
[ac_cv_struct_addrinfo=yes],

‎pyconfig.h.in‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,6 +2173,9 @@
21732173
/* Define if you have the 'tanpi' function. */
21742174
#undef _Py_HAVE_TANPI
21752175

2176+
/* Define if time.h declares the timezone, daylight and tzname variables. */
2177+
#undef _Py_HAVE_TIMEZONE_GLOBALS
2178+
21762179
/* Thread stack size set by the linker (in bytes). */
21772180
#undef _Py_LINKER_THREAD_STACK_SIZE
21782181

0 commit comments

Comments
 (0)