Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Doc/library/time.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,9 @@ Timezone Constants

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

.. data:: daylight

Expand Down
40 changes: 28 additions & 12 deletions Lib/test/test_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,18 +474,34 @@ def test_timezone(self):
self.assertEqual(strp_output.tm_isdst, 0)
strp_output = _strptime._strptime_time("GMT", "%Z")
self.assertEqual(strp_output.tm_isdst, 0)
time_tuple = time.localtime()
strf_output = time.strftime("%Z") #UTC does not have a timezone
strp_output = _strptime._strptime_time(strf_output, "%Z")
locale_time = _strptime.LocaleTime()
if time.tzname[0] != time.tzname[1] or not time.daylight:
self.assertTrue(strp_output[8] == time_tuple[8],
"timezone check failed; '%s' -> %s != %s" %
(strf_output, strp_output[8], time_tuple[8]))
else:
self.assertTrue(strp_output[8] == -1,
"LocaleTime().timezone has duplicate values and "
"time.daylight but timezone value not set to -1")
# %Z recognizes the module's timezone names, which need not match
# localtime() for every date in a zone whose rules have changed.
for isdst, name in enumerate(time.tzname):
if not name or (isdst and not time.daylight):
continue
with self.subTest(name=name, isdst=isdst):
if name.lower() in ('utc', 'gmt'):
expected = 0
elif time.daylight and time.tzname[0] == time.tzname[1]:
expected = -1
else:
expected = isdst
parsed = _strptime._strptime_time(name, '%Z')
self.assertEqual(parsed.tm_isdst, expected)

@support.run_with_tz('EST5EDT,M3.2.0,M11.1.0')
@skip_if_buggy_ucrt_strfptime
def test_timezone_roundtrip(self):
# Use stable, explicit rules when comparing strftime(), strptime(),
# and localtime(), including both standard time and DST.
for month, isdst in ((1, 0), (7, 1)):
with self.subTest(month=month):
t = time.mktime((2000, month, 1, 12, 0, 0, 0, 0, -1))
local = time.localtime(t)
self.assertEqual(local.tm_isdst, isdst)
name = time.strftime('%Z', local)
parsed = _strptime._strptime_time(name, '%Z')
self.assertEqual(parsed.tm_isdst, isdst)

@unittest.skipUnless(
hasattr(time, "tzset"), "time module has no attribute tzset"
Expand Down
32 changes: 25 additions & 7 deletions Lib/test/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,16 @@ def test_tzset(self):
del environ['TZ']
time.tzset()

@unittest.skipUnless(sysconfig.get_config_var('_Py_HAVE_TIMEZONE_GLOBALS'),
'requires libc timezone globals')
@support.run_with_tz('IST-1GMT0,M10.5.0,M3.5.0/1')
def test_tzset_negative_dst(self):
# Prefer libc's standard/DST classification over ordering offsets.
# altzone may still assume a one-hour positive DST shift.
self.assertEqual(time.timezone, -3600)
self.assertEqual(time.tzname, ('IST', 'GMT'))
self.assertEqual(time.daylight, 1)

def test_insane_timestamps(self):
# It's possible that some platform maps time_t to double,
# and that this test will fail there. This test should
Expand Down Expand Up @@ -858,17 +868,10 @@ def test_localtime_timezone(self):
lt = time.localtime()
self.assertHasAttr(lt, "tm_gmtoff")
self.assertHasAttr(lt, "tm_zone")

# See if the offset and zone are similar to the module
# attributes.
if lt.tm_gmtoff is None:
self.assertNotHasAttr(time, "timezone")
else:
self.assertEqual(lt.tm_gmtoff, -[time.timezone, time.altzone][lt.tm_isdst])
if lt.tm_zone is None:
self.assertNotHasAttr(time, "tzname")
else:
self.assertEqual(lt.tm_zone, time.tzname[lt.tm_isdst])

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

@support.run_with_tz('EST5EDT,M3.2.0,M11.1.0')
@skip_if_buggy_ucrt_strfptime
def test_localtime_timezone_constants(self):
# The module constants cannot describe every historical or current
# offset in zones whose rules have changed. Compare them against
# localtime() using explicit rules with a one-hour DST shift.
for month, isdst in ((1, 0), (7, 1)):
with self.subTest(month=month):
t = time.mktime((2000, month, 1, 12, 0, 0, 0, 0, -1))
lt = time.localtime(t)
self.assertEqual(lt.tm_isdst, isdst)
self.assertEqual(lt.tm_gmtoff,
-(time.timezone, time.altzone)[isdst])
self.assertEqual(lt.tm_zone, time.tzname[isdst])

def test_strptime_timezone(self):
t = time.strptime("UTC", "%Z")
self.assertEqual(t.tm_zone, 'UTC')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Use libc's ``timezone``, ``daylight`` and ``tzname`` variables for the
:mod:`time` module constants when available, rather than estimating them
from January and July samples. This fixes stale constants after timezone
rule changes and the standard/DST classification for negative DST zones.
The existing one-hour approximation for :data:`time.altzone` is unchanged
on platforms without a native ``altzone`` variable.
11 changes: 6 additions & 5 deletions Modules/timemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1854,7 +1854,7 @@ time_get_clock_info_impl(PyObject *module, const char *name)
return NULL;
}

#ifndef HAVE_DECL_TZNAME
#ifndef _Py_HAVE_TIMEZONE_GLOBALS
static void
get_zone(char *zone, int n, struct tm *p)
{
Expand All @@ -1875,7 +1875,7 @@ get_gmtoff(time_t t, struct tm *p)
return timegm(p) - t;
#endif
}
#endif // !HAVE_DECL_TZNAME
#endif // !_Py_HAVE_TIMEZONE_GLOBALS

static int
init_timezone(PyObject *m)
Expand Down Expand Up @@ -1904,7 +1904,7 @@ init_timezone(PyObject *m)

And I'm lazy and hate C so nyer.
*/
#ifdef HAVE_DECL_TZNAME
#ifdef _Py_HAVE_TIMEZONE_GLOBALS
PyObject *otz0, *otz1;
#if !defined(MS_WINDOWS) || defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
tzset();
Expand All @@ -1913,6 +1913,7 @@ init_timezone(PyObject *m)
#ifdef HAVE_ALTZONE
ADD_INT("altzone", altzone);
#else
/* Assume a one-hour DST shift when libc does not provide altzone. */
ADD_INT("altzone", _Py_timezone-3600);
#endif
ADD_INT("daylight", _Py_daylight);
Expand Down Expand Up @@ -1942,7 +1943,7 @@ init_timezone(PyObject *m)
if (PyModule_Add(m, "tzname", Py_BuildValue("(NN)", otz0, otz1)) < 0) {
return -1;
}
#else // !HAVE_DECL_TZNAME
#else // !_Py_HAVE_TIMEZONE_GLOBALS
static const time_t YEAR = (365 * 24 + 6) * 3600;
time_t t;
struct tm p;
Expand Down Expand Up @@ -1987,7 +1988,7 @@ init_timezone(PyObject *m)
if (PyModule_Add(m, "tzname", tzname_obj) < 0) {
return -1;
}
#endif // !HAVE_DECL_TZNAME
#endif // !_Py_HAVE_TIMEZONE_GLOBALS
#undef ADD_INT

if (PyErr_Occurred()) {
Expand Down
5 changes: 2 additions & 3 deletions PC/pyconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,8 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */
/* Define to 1 if you have the <direct.h> header file. */
#define HAVE_DIRECT_H 1

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

/* Define if you have dirent.h. */
/* #define DIRENT 1 */
Expand Down
43 changes: 43 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -6480,6 +6480,22 @@ if test $ac_cv_header_time_altzone = yes; then
[Define this if your time.h defines altzone.])
fi

AC_CACHE_CHECK([for time.h that defines timezone, daylight and tzname variables],
[ac_cv_header_time_tz_globals], [
AC_LINK_IFELSE([AC_LANG_PROGRAM([[@%:@include <time.h>]], [[
long tz = timezone;
int dl = daylight;
char *name = tzname[0];
return (int)tz + dl + (name != 0);
]])],
[ac_cv_header_time_tz_globals=yes],
[ac_cv_header_time_tz_globals=no])
])
if test $ac_cv_header_time_tz_globals = yes; then
AC_DEFINE([_Py_HAVE_TIMEZONE_GLOBALS], [1],
[Define if time.h declares the timezone, daylight and tzname variables.])
fi

AC_CACHE_CHECK([for addrinfo], [ac_cv_struct_addrinfo],
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[@%:@include <netdb.h>]], [[struct addrinfo a]])],
[ac_cv_struct_addrinfo=yes],
Expand Down
3 changes: 3 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -2173,6 +2173,9 @@
/* Define if you have the 'tanpi' function. */
#undef _Py_HAVE_TANPI

/* Define if time.h declares the timezone, daylight and tzname variables. */
#undef _Py_HAVE_TIMEZONE_GLOBALS

/* Thread stack size set by the linker (in bytes). */
#undef _Py_LINKER_THREAD_STACK_SIZE

Expand Down
Loading