Skip to content
Open
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
14 changes: 10 additions & 4 deletions Doc/library/time.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,10 @@ Timezone Constants

.. data:: daylight

Nonzero if a DST timezone is defined. See note below.
Nonzero if a DST timezone is defined. On some platforms, this includes
timezones that observed DST only in the past. It does not indicate whether
DST is currently in effect; use :attr:`struct_time.tm_isdst` from
:func:`localtime` for that. See note below.

.. data:: timezone

Expand All @@ -1047,9 +1050,12 @@ Timezone Constants

For the above Timezone constants (:data:`altzone`, :data:`daylight`, :data:`timezone`,
and :data:`tzname`), the value is determined by the timezone rules in effect
at module load time or the last time :func:`tzset` is called and may be incorrect
for times in the past. It is recommended to use the :attr:`~struct_time.tm_gmtoff` and
:attr:`~struct_time.tm_zone` results from :func:`localtime` to obtain timezone information.
at module load time or the last time :func:`tzset` is called and may be
incorrect for times in the past. On platforms that do not provide a DST
offset, :data:`altzone` is estimated by sampling dates in the current year.
It is recommended to use the :attr:`~struct_time.tm_gmtoff` and
:attr:`~struct_time.tm_zone` results from :func:`localtime` to obtain
timezone information.


.. seealso::
Expand Down
82 changes: 81 additions & 1 deletion Lib/test/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import fractions
import math
import platform
import struct
import sys
import sysconfig
import tempfile
import time
import threading
import unittest
Expand Down Expand Up @@ -509,7 +511,7 @@ def test_tzset(self):
try:
# Make sure we can switch to UTC time and results are correct
# Note that unknown timezones default to UTC.
# Note that altzone is undefined in UTC, as there is no DST
# Note that altzone equals timezone in UTC, as there is no DST
environ['TZ'] = eastern
time.tzset()
environ['TZ'] = utc
Expand All @@ -519,6 +521,7 @@ def test_tzset(self):
)
self.assertEqual(time.daylight, 0)
self.assertEqual(time.timezone, 0)
self.assertEqual(time.altzone, 0)
self.assertEqual(time.localtime(xmas2002).tm_isdst, 0)

# Make sure we can switch to US/Eastern
Expand Down Expand Up @@ -561,6 +564,83 @@ def test_tzset(self):
del environ['TZ']
time.tzset()

@support.run_with_tz('LHST-10:30LHDT-11,M10.1.0,M4.1.0')
def test_tzset_half_hour_dst(self):
self.assertEqual(time.timezone, -37800)
self.assertEqual(time.altzone, -39600)
self.assertEqual(time.daylight, 1)

@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):
self.assertEqual(time.timezone, -3600)
self.assertEqual(time.altzone, 0)
self.assertEqual(time.daylight, 1)
self.assertEqual(time.tzname, ('IST', 'GMT'))

@unittest.skipUnless(sysconfig.get_config_var('_Py_HAVE_TIMEZONE_GLOBALS')
and not sysconfig.get_config_var('HAVE_ALTZONE'),
'requires sampling the DST offset')
@support.run_with_tz('Asia/Kolkata')
def test_tzset_historical_dst(self):
# Kolkata observed DST during World War II, but not since 1945.
# Check libc's zone data, rather than assuming it is installed.
historical = time.localtime(-862531200) # 1942-09-02 UTC
if (historical.tm_isdst != 1
or getattr(historical, 'tm_gmtoff', None) != 23400):
self.skipTest('requires historical Asia/Kolkata timezone data')
current = time.localtime()
if (current.tm_isdst != 0
or getattr(current, 'tm_gmtoff', None) != 19800):
self.skipTest('requires Asia/Kolkata without current DST')
self.assertEqual(time.timezone, -19800)
# libc's daylight may still be nonzero for the historical DST.
self.assertEqual(time.altzone, time.timezone)

@unittest.skipUnless(sysconfig.get_config_var('_Py_HAVE_TIMEZONE_GLOBALS')
and not sysconfig.get_config_var('HAVE_ALTZONE')
and sysconfig.get_config_var('HAVE_MKTIME'),
'requires calendar-based DST offset sampling')
def test_tzset_recent_dst_offset(self):
# A synthetic TZif zone has an old, a current, and a future DST
# offset. Prefer the current offset over both other offsets.
now = int(time.time())
transitions = ((now - 370 * 86400, 1), (now - 3600, 2),
(now + 86400, 3))
self.check_tzset_dst_offset(transitions, -7200)

def check_tzset_dst_offset(self, transitions, expected):
names = b'STD\0OLD\0NEW\0FUT\0'

def header(timecnt, typecnt, charcnt):
return (b'TZif2' + bytes(15)
+ struct.pack('>6l', 0, 0, 0, timecnt, typecnt, charcnt))

data = (header(0, 1, 4) + struct.pack('>lbb', 0, 0, 0) + b'STD\0'
+ header(len(transitions), 4, len(names))
+ struct.pack(f'>{len(transitions)}q',
*(t for t, _ in transitions))
+ bytes(index for _, index in transitions))
for offset, isdst, index in ((0, 0, 0), (3600, 1, 4),
(7200, 1, 8), (10800, 1, 12)):
data += struct.pack('>lbb', offset, isdst, index)
data += names + b'\n\n'

with tempfile.NamedTemporaryFile() as zone:
zone.write(data)
zone.flush()

@support.run_with_tz(':' + zone.name)
def check():
for t, index in transitions:
local = time.localtime(t)
if getattr(local, 'tm_gmtoff', None) != index * 3600:
self.skipTest('requires TZif v2 timezone support')
self.assertEqual(time.altzone, expected)

check()

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
On platforms where libc provides the ``timezone``, ``daylight`` and
``tzname`` variables, :data:`time.timezone`, :data:`time.daylight` and
:data:`time.tzname` are now taken from libc instead of being guessed from
samples of the local time in January and July. This fixes stale values for
zones whose rules change during the year, such as ``America/Vancouver`` in
2026. :data:`time.altzone` now equals :data:`time.timezone` for zones
without DST when libc does not provide ``altzone``.
108 changes: 98 additions & 10 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 @@ -1865,7 +1865,10 @@ get_zone(char *zone, int n, struct tm *p)
strftime(zone, n, "%Z", p);
#endif
}
#endif // !_Py_HAVE_TIMEZONE_GLOBALS

#if !defined(_Py_HAVE_TIMEZONE_GLOBALS) || \
(!defined(HAVE_ALTZONE) && defined(HAVE_MKTIME))
static time_t
get_gmtoff(time_t t, struct tm *p)
{
Expand All @@ -1875,7 +1878,80 @@ get_gmtoff(time_t t, struct tm *p)
return timegm(p) - t;
#endif
}
#endif // !HAVE_DECL_TZNAME
#endif

#if defined(_Py_HAVE_TIMEZONE_GLOBALS) && !defined(HAVE_ALTZONE) && defined(HAVE_MKTIME)
static long
get_dst_offset(time_t now, long dst_offset)
{
/* Sample calendar months in the current year, looking for an actual
DST offset, which need not be one hour from standard time.
Prefer the latest DST sample at or before now; only look ahead
if none has been found by the current month. If none is found
at all, use the standard offset. This is only a heuristic:
short DST seasons may not be sampled. */
struct tm current;
if (now == (time_t)-1) {
return dst_offset;
}
if (_PyTime_localtime(now, &current) < 0) {
PyErr_Clear();
return dst_offset;
}
if (current.tm_mon < 0 || current.tm_mon > 11) {
return dst_offset;
}

int found_dst = 0;
for (int month = 0; month < 12; month++) {
time_t sample;
struct tm p;
if (month == current.tm_mon) {
sample = now;
p = current;
}
else {
p = (struct tm){
.tm_year = current.tm_year,
.tm_mon = month,
.tm_mday = 15,
.tm_hour = 12,
.tm_isdst = -1,
.tm_wday = -1,
};
sample = mktime(&p);
/* As in time_mktime(), -1 alone does not imply failure. */
if (sample == (time_t)-1 && p.tm_wday == -1) {
break;
}
if (_PyTime_localtime(sample, &p) < 0) {
PyErr_Clear();
break;
}
/* mktime() may normalize a nonexistent local date. */
if (p.tm_year != current.tm_year || p.tm_mon != month
|| (month < current.tm_mon && sample > now)
|| (month > current.tm_mon && sample < now))
{
break;
}
}
if (p.tm_isdst > 0) {
errno = 0;
time_t gmtoff = get_gmtoff(sample, &p);
if (errno != 0 || gmtoff < -48 * 3600 || gmtoff > 48 * 3600) {
break;
}
dst_offset = -(long)gmtoff;
found_dst = 1;
}
if (month >= current.tm_mon && found_dst) {
break;
}
}
return dst_offset;
}
#endif

static int
init_timezone(PyObject *m)
Expand Down Expand Up @@ -1904,18 +1980,19 @@ 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();
#endif
ADD_INT("timezone", _Py_timezone);
#ifdef HAVE_ALTZONE
ADD_INT("altzone", altzone);
#else
ADD_INT("altzone", _Py_timezone-3600);
#endif
ADD_INT("daylight", _Py_daylight);
#ifndef HAVE_ALTZONE
long dst_offset = _Py_timezone;
#ifdef HAVE_MKTIME
int has_dst = _Py_daylight;
#endif
#endif
#ifdef MS_WINDOWS
TIME_ZONE_INFORMATION tzinfo = {0};
GetTimeZoneInformation(&tzinfo);
Expand All @@ -1942,7 +2019,18 @@ init_timezone(PyObject *m)
if (PyModule_Add(m, "tzname", Py_BuildValue("(NN)", otz0, otz1)) < 0) {
return -1;
}
#else // !HAVE_DECL_TZNAME
#ifdef HAVE_ALTZONE
ADD_INT("altzone", altzone);
#else
#ifdef HAVE_MKTIME
if (has_dst) {
// Sampling is best-effort: retain the last offset on failure.
dst_offset = get_dst_offset(time(NULL), dst_offset);
}
#endif
ADD_INT("altzone", dst_offset);
#endif
#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 +2075,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