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
41 changes: 29 additions & 12 deletions Lib/test/test_strptime.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,18 +474,35 @@ 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. UTC and GMT are always
# parsed as standard time, even in zones such as Europe/Dublin where
# libc reports GMT as (negative) DST.
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
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,14 @@
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. Standard and DST are now classified by libc rather than by comparing
offsets, so for zones with negative DST such as ``Europe/Dublin``,
:data:`time.tzname` is now ``('IST', 'GMT')`` and :data:`time.timezone` is
``-3600``, matching :attr:`~time.struct_time.tm_isdst`. On some platforms
:data:`time.daylight` may now be nonzero for zones that observed DST only in
the past. When libc does not provide ``altzone`` (including glibc and
Windows), :data:`time.altzone` is now found by sampling the current year for
a DST offset instead of assuming a one-hour shift, and equals
:data:`time.timezone` for zones without DST.
111 changes: 101 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,77 @@ 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;
}

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))
{
continue;
}
}
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 +1977,25 @@ 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)
/* glibc caveat: tzset() returns early if TZ is unchanged since the
last call, and localtime() updates tzname and daylight for the rules
in effect at the converted time. So when time.tzset() is called
without changing TZ, the values read here reflect the most recent
localtime() call rather than a fresh read of the zone data. There
is no portable way to force a re-read. */
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 +2022,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 +2078,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
Loading
Loading