diff --git a/Doc/library/time.rst b/Doc/library/time.rst index b0d9e07ba89..dc102d60270 100644 --- a/Doc/library/time.rst +++ b/Doc/library/time.rst @@ -24,9 +24,9 @@ An explanation of some terminology and conventions is in order. .. index:: single: Year 2038 -* The functions in this module do not handle dates and times before the epoch or +* The functions in this module may not handle dates and times before the epoch or far in the future. The cut-off point in the future is determined by the C - library; for Unix, it is typically in 2038. + library; for 32-bit systems, it is typically in 2038. .. index:: single: Year 2000 @@ -34,20 +34,31 @@ An explanation of some terminology and conventions is in order. .. _time-y2kissues: -* **Year 2000 (Y2K) issues**: Python depends on the platform's C library, which +* **Year 2000 (Y2K) issues**: Python depends on the platform's C library, which generally doesn't have year 2000 issues, since all dates and times are - represented internally as seconds since the epoch. Functions accepting a - :class:`struct_time` (see below) generally require a 4-digit year. For backward - compatibility, 2-digit years are supported if the module variable - ``accept2dyear`` is a non-zero integer; this variable is initialized to ``1`` - unless the environment variable :envvar:`PYTHONY2K` is set to a non-empty - string, in which case it is initialized to ``0``. Thus, you can set - :envvar:`PYTHONY2K` to a non-empty string in the environment to require 4-digit - years for all year input. When 2-digit years are accepted, they are converted - according to the POSIX or X/Open standard: values 69-99 are mapped to 1969-1999, - and values 0--68 are mapped to 2000--2068. Values 100--1899 are always illegal. - Note that this is new as of Python 1.5.2(a2); earlier versions, up to Python - 1.5.1 and 1.5.2a1, would add 1900 to year values below 1900. + represented internally as seconds since the epoch. Function :func:`strptime` + can parse 2-digit years when given ``%y`` format code. When 2-digit years are + parsed, they are converted according to the POSIX and ISO C standards: values + 69--99 are mapped to 1969--1999, and values 0--68 are mapped to 2000--2068. + + For backward compatibility, years with less than 4 digits are treated + specially by :func:`asctime`, :func:`mktime`, and :func:`strftime` functions + that operate on a 9-tuple or :class:`struct_time` values. If year (the first + value in the 9-tuple) is specified with less than 4 digits, its interpretation + depends on the value of ``accept2dyear`` variable. + + If ``accept2dyear`` is true (default), a backward compatibility behavior is + invoked as follows: + + - for 2-digit year, century is guessed according to POSIX rules for + ``%y`` strptime format. A deprecation warning is issued when century + information is guessed in this way. + + - for 3-digit or negative year, a :exc:`ValueError` exception is raised. + + If ``accept2dyear`` is false (set by the program or as a result of a + non-empty value assigned to ``PYTHONY2K`` environment variable) all year + values are interpreted as given. .. index:: single: UTC diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index fca221ce9cb..e4b6a0fb465 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -3,6 +3,7 @@ import time import unittest import locale import sysconfig +import warnings class TimeTestCase(unittest.TestCase): @@ -19,10 +20,10 @@ class TimeTestCase(unittest.TestCase): time.clock() def test_conversions(self): - self.assertTrue(time.ctime(self.t) - == time.asctime(time.localtime(self.t))) - self.assertTrue(int(time.mktime(time.localtime(self.t))) - == int(self.t)) + self.assertEqual(time.ctime(self.t), + time.asctime(time.localtime(self.t))) + self.assertEqual(int(time.mktime(time.localtime(self.t))), + int(self.t)) def test_sleep(self): time.sleep(1.2) @@ -44,7 +45,7 @@ class TimeTestCase(unittest.TestCase): # Check year [1900, max(int)] self.assertRaises(ValueError, func, - (1899, 1, 1, 0, 0, 0, 0, 1, -1)) + (999, 1, 1, 0, 0, 0, 0, 1, -1)) if time.accept2dyear: self.assertRaises(ValueError, func, (-1, 1, 1, 0, 0, 0, 0, 1, -1)) @@ -97,7 +98,8 @@ class TimeTestCase(unittest.TestCase): # No test for daylight savings since strftime() does not change output # based on its value. expected = "2000 01 01 00 00 00 1 001" - result = time.strftime("%Y %m %d %H %M %S %w %j", (0,)*9) + with support.check_warnings(): + result = time.strftime("%Y %m %d %H %M %S %w %j", (0,)*9) self.assertEqual(expected, result) def test_strptime(self): @@ -141,14 +143,15 @@ class TimeTestCase(unittest.TestCase): self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973') t = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, -1)) self.assertEqual(time.ctime(t), 'Sat Jan 1 00:00:00 2000') - try: - bigval = time.mktime((10000, 1, 10) + (0,)*6) - except (ValueError, OverflowError): - # If mktime fails, ctime will fail too. This may happen - # on some platforms. - pass - else: - self.assertEqual(time.ctime(bigval)[-5:], '10000') + for year in [-100, 100, 1000, 2000, 10000]: + try: + testval = time.mktime((year, 1, 10) + (0,)*6) + except (ValueError, OverflowError): + # If mktime fails, ctime will fail too. This may happen + # on some platforms. + pass + else: + self.assertEqual(time.ctime(testval)[20:], str(year)) @unittest.skipIf(not hasattr(time, "tzset"), "time module has no attribute tzset") @@ -239,14 +242,14 @@ class TimeTestCase(unittest.TestCase): gt1 = time.gmtime(None) t0 = time.mktime(gt0) t1 = time.mktime(gt1) - self.assertTrue(0 <= (t1-t0) < 0.2) + self.assertAlmostEqual(t1, t0, delta=0.2) def test_localtime_without_arg(self): lt0 = time.localtime() lt1 = time.localtime(None) t0 = time.mktime(lt0) t1 = time.mktime(lt1) - self.assertTrue(0 <= (t1-t0) < 0.2) + self.assertAlmostEqual(t1, t0, delta=0.2) class TestLocale(unittest.TestCase): def setUp(self): @@ -274,16 +277,18 @@ class TestAccept2Year(unittest.TestCase): time.accept2dyear = self.saved_accept2dyear def yearstr(self, y): - return time.strftime('%Y', (y,) + (0,) * 8) + # return time.strftime('%Y', (y,) + (0,) * 8) + return time.asctime((y,) + (0,) * 8).split()[-1] def test_2dyear(self): - self.assertEqual(self.yearstr(0), '2000') - self.assertEqual(self.yearstr(69), '1969') - self.assertEqual(self.yearstr(68), '2068') - self.assertEqual(self.yearstr(99), '1999') + with support.check_warnings(): + self.assertEqual(self.yearstr(0), '2000') + self.assertEqual(self.yearstr(69), '1969') + self.assertEqual(self.yearstr(68), '2068') + self.assertEqual(self.yearstr(99), '1999') def test_invalid(self): - self.assertRaises(ValueError, self.yearstr, 1899) + self.assertRaises(ValueError, self.yearstr, 999) self.assertRaises(ValueError, self.yearstr, 100) self.assertRaises(ValueError, self.yearstr, -1) @@ -293,10 +298,15 @@ class TestAccept2YearBool(TestAccept2Year): class TestDontAccept2Year(TestAccept2Year): accept2dyear = 0 def test_2dyear(self): - self.assertRaises(ValueError, self.yearstr, 0) - self.assertRaises(ValueError, self.yearstr, 69) - self.assertRaises(ValueError, self.yearstr, 68) - self.assertRaises(ValueError, self.yearstr, 99) + self.assertEqual(self.yearstr(0), '0') + self.assertEqual(self.yearstr(69), '69') + self.assertEqual(self.yearstr(68), '68') + self.assertEqual(self.yearstr(99), '99') + self.assertEqual(self.yearstr(999), '999') + self.assertEqual(self.yearstr(9999), '9999') + + def test_invalid(self): + pass class TestDontAccept2YearBool(TestDontAccept2Year): accept2dyear = False diff --git a/Misc/NEWS b/Misc/NEWS index 33aac6e3db0..01c25426d1b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -36,6 +36,14 @@ Core and Builtins Library ------- +- Issue #10827: Changed the rules for 2-digit years. The time.asctime + function will now format any year when ``time.accept2dyear`` is + false and will accept years >= 1000 otherwise. The year range + accepted by ``time.mktime`` and ``time.strftime`` is still system + dependent, but ``time.mktime`` will now accept full range supported + by the OS. Conversion of 2-digit years to 4-digit is deprecated. + + - Issue #7858: Raise an error properly when os.utime() fails under Windows on an existing file. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 4b45f6e30d3..94265c6b2c8 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -312,34 +312,42 @@ gettmarg(PyObject *args, struct tm *p) &p->tm_wday, &p->tm_yday, &p->tm_isdst)) return 0; - /* XXX: Why 1900? If the goal is to interpret 2-digit years as those in - * 20th / 21st century according to the POSIX standard, we can just treat - * 0 <= y < 100 as special. Year 100 is probably too ambiguous and should - * be rejected, but years 101 through 1899 can be passed through. + /* If year is specified with less than 4 digits, its interpretation + * depends on the accept2dyear value. + * + * If accept2dyear is true (default), a backward compatibility behavior is + * invoked as follows: + * + * - for 2-digit year, century is guessed according to POSIX rules for + * %y strptime format: 21st century for y < 69, 20th century + * otherwise. A deprecation warning is issued when century + * information is guessed in this way. + * + * - for 3-digit or negative year, a ValueError exception is raised. + * + * If accept2dyear is false (set by the program or as a result of a + * non-empty value assigned to PYTHONY2K environment variable) all year + * values are interpreted as given. */ - if (y < 1900) { + if (y < 1000) { PyObject *accept = PyDict_GetItemString(moddict, "accept2dyear"); int acceptval = accept != NULL && PyObject_IsTrue(accept); if (acceptval == -1) return 0; if (acceptval) { - if (69 <= y && y <= 99) - y += 1900; - else if (0 <= y && y <= 68) + if (0 <= y && y < 69) y += 2000; + else if (69 <= y && y < 100) + y += 1900; else { PyErr_SetString(PyExc_ValueError, "year out of range"); return 0; } - } - /* XXX: When accept2dyear is false, we don't have to reject y < 1900. - * Consider removing the following else-clause. */ - else { - PyErr_SetString(PyExc_ValueError, - "year out of range"); - return 0; + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Century info guessed for a 2-digit year.", 1) != 0) + return 0; } } p->tm_year = y - 1900; @@ -462,6 +470,15 @@ time_strftime(PyObject *self, PyObject *args) else if (!gettmarg(tup, &buf) || !checktm(&buf)) return NULL; + /* XXX: Reportedly, some systems have issues formating dates prior to year + * 1900. These systems should be identified and this check should be + * moved to appropriate system specific section below. */ + if (buf.tm_year < 0) { + PyErr_Format(PyExc_ValueError, "year=%d is before 1900; " + "the strftime() method requires year >= 1900", + buf.tm_year + 1900); + } + /* Normalize tm_isdst just in case someone foolishly implements %Z based on the assumption that tm_isdst falls within the range of [-1, 1] */