diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -2,9 +2,12 @@ import time import unittest import locale +import os import sysconfig import sys import platform +from calendar import timegm +from itertools import product try: import threading except ImportError: @@ -471,6 +474,95 @@ self.assertRaises(ValueError, time.get_clock_info, 'xxx') +class TestTimezoneInfoIsUpdated(unittest.TestCase): + + def _assert_tzinfo_in_sync(self): + # verify that time module timezone attributes correspond to C variables + from ctypes import c_char_p, c_int, c_long, pythonapi + + skipped = [] + try: + tzname_c = (c_char_p*2).in_dll(pythonapi, "tzname") + except ValueError: + skipped.append('tzname') + else: #NOTE: the encoding is not clear, Issue16322 + self.assertEqual(tuple(map(os.fsdecode, tzname_c)), time.tzname) + + try: + daylight_c = c_int.in_dll(pythonapi, "daylight").value + except ValueError: + skipped.append('daylight') + else: + self.assertEqual(time.daylight, daylight_c) + + try: + timezone_c = c_long.in_dll(pythonapi, "timezone").value + except ValueError: + skipped.append('timezone') + else: + self.assertEqual(time.timezone, timezone_c) + + try: + altzone_c = c_long.in_dll(pythonapi, "altzone").value + except ValueError: + pass #NOTE: ignore it + else: + self.assertEqual(time.altzone, altzone_c) + + if skipped: + self.skipTest("%s C variable/s is/are not available" % + ', '.join(skipped)) + + # test with Olson's timezone + @unittest.skipUnless(any(os.path.exists(path) for path in [ + # use http://wiki.tcl.tk/14853 paths + "/usr/share/zoneinfo", + "/user/share/lib/zoneinfo", + "/user/lib/zoneinfo", + "/user/local/etc/zoneinfo", + "C:/Progra~1/cygwin/usr/local/etc/zoneinfo" + ]), + "Can't find IANA's Time Zone Database") + @support.run_with_tz('Europe/Moscow') + @support.cpython_only + def test_timezone_with_multiple_tzinfos(self): + # test whether Python and C timezone info are in sync. + + # ctime, localtime, mktime, strftime -- should update time attributes + # asctime, gmtime -- should not update time attribute + #NOTE: the actual updating behavior might differ + #from the specified + # tzset -- updates time attributes according to the current (year) time + time_tuple = lambda date: date + (0,)*3 + (-1, 1, -1) + time_func = { # func_name -> date -> call + 'tzset': lambda date: time.tzset(), + 'ctime': lambda date: time.ctime(timegm(time_tuple(date))), + 'localtime': lambda date: time.localtime(timegm(time_tuple(date))), + 'mktime': lambda date: time.mktime(time_tuple(date)), + 'strftime': lambda date: time.strftime('%Z', time_tuple(date)), + 'asctime': lambda date: time.asctime(time_tuple(date)), + 'gmtime': lambda date: time.gmtime(timegm(time_tuple(date))), + } + # use dates with different timezone info (different zone, gmtoff) + dates = { + # Year January July December + # 2010 MSK+0300 MSD+0400 MSK+0300 + (2010, 1, 1), (2010, 7, 1), (2010, 12, 1), + # 2011 MSK+0300 MSK+0400 MSK+0400 + (2011, 1, 1), (2011, 7, 1), (2011, 12, 1), + # 2013 MSK+0400 MSK+0400 MSK+0400 + (2013, 1, 1), (2013, 7, 1), (2013, 12, 1), + # 2014 MSK+0400 MSK+0400 MSK+0300 + (2014, 1, 1), (2014, 7, 1), (2014, 12, 1), + # # 2015 MSK+0300 MSK+0300 MSK+0300 + (2015, 1, 1), (2015, 7, 1), (2015, 12, 1), + } + for date, name in product(sorted(dates), sorted(time_func)): + with self.subTest(date=date, name=name): + time.tzset() # reset tzinfo to the current time + time_func[name](date) # possibly update time module attributes + self._assert_tzinfo_in_sync() + class TestLocale(unittest.TestCase): def setUp(self): self.oldloc = locale.setlocale(locale.LC_ALL) diff --git a/Modules/timemodule.c b/Modules/timemodule.c --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -366,6 +366,72 @@ attributes only."); static int +py_sync_timezone(PyObject* time_module, struct tm* timeptr) +{ + /* Synchronize time module timezone attributes with the + corresponding C timezone variables: tzname, timezone, daylight, + altzone. + + It should be called after tzset, ctime, localtime, mktime, + strftime C functions that may change these variables. + + This function does not call tzset(). + */ + int ret = 0; /* ok */ +#if defined(HAVE_TZNAME) || defined(HAVE_STRUCT_TM_TM_ZONE) + long timezone_ = 0, altzone_ = 0; + int daylight_ = 0; + const char *tz0 = NULL, *tz1 = NULL; + PyObject *otz0 = NULL, *otz1 = NULL; + + assert (timeptr != NULL); + if (time_module == NULL) { + time_module = PyImport_ImportModuleNoBlock("time"); /* get time module */ + if (time_module == NULL) { + return -1; /* error */ + } + } + else + Py_INCREF(time_module); + + /* update time.tzname, .timezone, .daylight .altzone attributes */ + /*NOTE: assume HAVE_TZNAME or HAVE_STRUCT_TM_TM_ZONE implies tzname, + timezone, daylight presence */ + tz0 = tzname[0]; + timezone_ = timezone; + daylight_ = daylight; + /* if no daylight; tzname[1] shouldn't be used. Set it to tzname[0] + defensively. */ + tz1 = daylight_ ? tzname[1] : tz0; +#ifdef HAVE_ALTZONE + altzone_ = altzone; +#else /* !HAVE_ALTZONE */ + /* daylight is nonzero if timezone has DST during a year (or ever). + + (timezone-3600) is a wrong altzone value for >10% timezones (all + time) or only for 0.5% (3 out of 582) timezones after 2014. + + If no daylight; altzone shouldn't be used. Set it to timezone + defensively. */ + altzone_ = daylight_ ? timezone_ - 3600 : timezone_; +#endif /* !HAVE_ALTZONE */ + + otz0 = PyUnicode_DecodeLocale(tz0, "surrogateescape"); + otz1 = PyUnicode_DecodeLocale(tz1, "surrogateescape"); + if (PyModule_AddObject(time_module, "tzname", + Py_BuildValue("(NN)", otz0, otz1)) || + PyModule_AddIntConstant(time_module, "timezone", timezone_) || + PyModule_AddIntConstant(time_module, "daylight", daylight_) || + PyModule_AddIntConstant(time_module, "altzone", altzone_)) + ret = -1; /* error */ + Py_DECREF(time_module); +#endif /* defined(HAVE_TZNAME) || defined(HAVE_STRUCT_TM_TM_ZONE) */ + /*XXX __CYGWIN__ clause is removed to avoid dead code. I don't see + cygwin buildbots. */ + return ret; +} + +static int pylocaltime(time_t *timep, struct tm *result) { struct tm *local; @@ -381,6 +447,9 @@ PyErr_SetFromErrno(PyExc_OSError); return -1; } + else if (py_sync_timezone(NULL, local) == -1) + return -1; + *result = *local; return 0; } @@ -697,6 +766,10 @@ #else Py_DECREF(format); #endif + if (ret != NULL && py_sync_timezone(NULL, &buf) == -1) { + Py_DECREF(ret); + return NULL; + } return ret; } @@ -837,6 +910,8 @@ "mktime argument out of range"); return NULL; } + if (py_sync_timezone(NULL, &buf) == -1) + return NULL; return PyFloat_FromDouble((double)tt); } @@ -1152,84 +1227,10 @@ static void PyInit_timezone(PyObject *m) { - /* This code moved from PyInit_time wholesale to allow calling it from - time_tzset. In the future, some parts of it can be moved back - (for platforms that don't HAVE_WORKING_TZSET, when we know what they - are), and the extraneous calls to tzset(3) should be removed. - I haven't done this yet, as I don't want to change this code as - little as possible when introducing the time.tzset and time.tzsetwall - methods. This should simply be a method of doing the following once, - at the top of this function and removing the call to tzset() from - time_tzset(): - - #ifdef HAVE_TZSET - tzset() - #endif - - And I'm lazy and hate C so nyer. - */ -#if defined(HAVE_TZNAME) && !defined(__GLIBC__) && !defined(__CYGWIN__) - PyObject *otz0, *otz1; - tzset(); - PyModule_AddIntConstant(m, "timezone", timezone); -#ifdef HAVE_ALTZONE - PyModule_AddIntConstant(m, "altzone", altzone); -#else - PyModule_AddIntConstant(m, "altzone", timezone-3600); -#endif - PyModule_AddIntConstant(m, "daylight", daylight); - otz0 = PyUnicode_DecodeLocale(tzname[0], "surrogateescape"); - otz1 = PyUnicode_DecodeLocale(tzname[1], "surrogateescape"); - PyModule_AddObject(m, "tzname", Py_BuildValue("(NN)", otz0, otz1)); -#else /* !HAVE_TZNAME || __GLIBC__ || __CYGWIN__*/ -#ifdef HAVE_STRUCT_TM_TM_ZONE - { -#define YEAR ((time_t)((365 * 24 + 6) * 3600)) - time_t t; - struct tm *p; - long janzone, julyzone; - char janname[10], julyname[10]; - t = (time((time_t *)0) / YEAR) * YEAR; - p = localtime(&t); - janzone = -p->tm_gmtoff; - strncpy(janname, p->tm_zone ? p->tm_zone : " ", 9); - janname[9] = '\0'; - t += YEAR/2; - p = localtime(&t); - julyzone = -p->tm_gmtoff; - strncpy(julyname, p->tm_zone ? p->tm_zone : " ", 9); - julyname[9] = '\0'; - - if( janzone < julyzone ) { - /* DST is reversed in the southern hemisphere */ - PyModule_AddIntConstant(m, "timezone", julyzone); - PyModule_AddIntConstant(m, "altzone", janzone); - PyModule_AddIntConstant(m, "daylight", - janzone != julyzone); - PyModule_AddObject(m, "tzname", - Py_BuildValue("(zz)", - julyname, janname)); - } else { - PyModule_AddIntConstant(m, "timezone", janzone); - PyModule_AddIntConstant(m, "altzone", julyzone); - PyModule_AddIntConstant(m, "daylight", - janzone != julyzone); - PyModule_AddObject(m, "tzname", - Py_BuildValue("(zz)", - janname, julyname)); - } - } -#else -#endif /* HAVE_STRUCT_TM_TM_ZONE */ -#ifdef __CYGWIN__ - tzset(); - PyModule_AddIntConstant(m, "timezone", _timezone); - PyModule_AddIntConstant(m, "altzone", _timezone-3600); - PyModule_AddIntConstant(m, "daylight", _daylight); - PyModule_AddObject(m, "tzname", - Py_BuildValue("(zz)", _tzname[0], _tzname[1])); -#endif /* __CYGWIN__ */ -#endif /* !HAVE_TZNAME || __GLIBC__ || __CYGWIN__*/ + time_t ts = time(NULL); /* sync. to the current time */ + struct tm *timeptr = localtime(&ts); + if (timeptr) + py_sync_timezone(m, timeptr); #if defined(HAVE_CLOCK_GETTIME) PyModule_AddIntMacro(m, CLOCK_REALTIME);