mirror of https://github.com/python/cpython
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.
This commit is contained in:
parent
696efdd03f
commit
c64708ae48
|
@ -24,9 +24,9 @@ An explanation of some terminology and conventions is in order.
|
||||||
|
|
||||||
.. index:: single: Year 2038
|
.. 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
|
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::
|
.. index::
|
||||||
single: Year 2000
|
single: Year 2000
|
||||||
|
@ -34,20 +34,31 @@ An explanation of some terminology and conventions is in order.
|
||||||
|
|
||||||
.. _time-y2kissues:
|
.. _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
|
generally doesn't have year 2000 issues, since all dates and times are
|
||||||
represented internally as seconds since the epoch. Functions accepting a
|
represented internally as seconds since the epoch. Function :func:`strptime`
|
||||||
:class:`struct_time` (see below) generally require a 4-digit year. For backward
|
can parse 2-digit years when given ``%y`` format code. When 2-digit years are
|
||||||
compatibility, 2-digit years are supported if the module variable
|
parsed, they are converted according to the POSIX and ISO C standards: values
|
||||||
``accept2dyear`` is a non-zero integer; this variable is initialized to ``1``
|
69--99 are mapped to 1969--1999, and values 0--68 are mapped to 2000--2068.
|
||||||
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
|
For backward compatibility, years with less than 4 digits are treated
|
||||||
:envvar:`PYTHONY2K` to a non-empty string in the environment to require 4-digit
|
specially by :func:`asctime`, :func:`mktime`, and :func:`strftime` functions
|
||||||
years for all year input. When 2-digit years are accepted, they are converted
|
that operate on a 9-tuple or :class:`struct_time` values. If year (the first
|
||||||
according to the POSIX or X/Open standard: values 69-99 are mapped to 1969-1999,
|
value in the 9-tuple) is specified with less than 4 digits, its interpretation
|
||||||
and values 0--68 are mapped to 2000--2068. Values 100--1899 are always illegal.
|
depends on the value of ``accept2dyear`` variable.
|
||||||
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.
|
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::
|
.. index::
|
||||||
single: UTC
|
single: UTC
|
||||||
|
|
|
@ -3,6 +3,7 @@ import time
|
||||||
import unittest
|
import unittest
|
||||||
import locale
|
import locale
|
||||||
import sysconfig
|
import sysconfig
|
||||||
|
import warnings
|
||||||
|
|
||||||
class TimeTestCase(unittest.TestCase):
|
class TimeTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -19,10 +20,10 @@ class TimeTestCase(unittest.TestCase):
|
||||||
time.clock()
|
time.clock()
|
||||||
|
|
||||||
def test_conversions(self):
|
def test_conversions(self):
|
||||||
self.assertTrue(time.ctime(self.t)
|
self.assertEqual(time.ctime(self.t),
|
||||||
== time.asctime(time.localtime(self.t)))
|
time.asctime(time.localtime(self.t)))
|
||||||
self.assertTrue(int(time.mktime(time.localtime(self.t)))
|
self.assertEqual(int(time.mktime(time.localtime(self.t))),
|
||||||
== int(self.t))
|
int(self.t))
|
||||||
|
|
||||||
def test_sleep(self):
|
def test_sleep(self):
|
||||||
time.sleep(1.2)
|
time.sleep(1.2)
|
||||||
|
@ -44,7 +45,7 @@ class TimeTestCase(unittest.TestCase):
|
||||||
|
|
||||||
# Check year [1900, max(int)]
|
# Check year [1900, max(int)]
|
||||||
self.assertRaises(ValueError, func,
|
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:
|
if time.accept2dyear:
|
||||||
self.assertRaises(ValueError, func,
|
self.assertRaises(ValueError, func,
|
||||||
(-1, 1, 1, 0, 0, 0, 0, 1, -1))
|
(-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
|
# No test for daylight savings since strftime() does not change output
|
||||||
# based on its value.
|
# based on its value.
|
||||||
expected = "2000 01 01 00 00 00 1 001"
|
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)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
def test_strptime(self):
|
def test_strptime(self):
|
||||||
|
@ -141,14 +143,15 @@ class TimeTestCase(unittest.TestCase):
|
||||||
self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973')
|
self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973')
|
||||||
t = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, -1))
|
t = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, -1))
|
||||||
self.assertEqual(time.ctime(t), 'Sat Jan 1 00:00:00 2000')
|
self.assertEqual(time.ctime(t), 'Sat Jan 1 00:00:00 2000')
|
||||||
try:
|
for year in [-100, 100, 1000, 2000, 10000]:
|
||||||
bigval = time.mktime((10000, 1, 10) + (0,)*6)
|
try:
|
||||||
except (ValueError, OverflowError):
|
testval = time.mktime((year, 1, 10) + (0,)*6)
|
||||||
# If mktime fails, ctime will fail too. This may happen
|
except (ValueError, OverflowError):
|
||||||
# on some platforms.
|
# If mktime fails, ctime will fail too. This may happen
|
||||||
pass
|
# on some platforms.
|
||||||
else:
|
pass
|
||||||
self.assertEqual(time.ctime(bigval)[-5:], '10000')
|
else:
|
||||||
|
self.assertEqual(time.ctime(testval)[20:], str(year))
|
||||||
|
|
||||||
@unittest.skipIf(not hasattr(time, "tzset"),
|
@unittest.skipIf(not hasattr(time, "tzset"),
|
||||||
"time module has no attribute tzset")
|
"time module has no attribute tzset")
|
||||||
|
@ -239,14 +242,14 @@ class TimeTestCase(unittest.TestCase):
|
||||||
gt1 = time.gmtime(None)
|
gt1 = time.gmtime(None)
|
||||||
t0 = time.mktime(gt0)
|
t0 = time.mktime(gt0)
|
||||||
t1 = time.mktime(gt1)
|
t1 = time.mktime(gt1)
|
||||||
self.assertTrue(0 <= (t1-t0) < 0.2)
|
self.assertAlmostEqual(t1, t0, delta=0.2)
|
||||||
|
|
||||||
def test_localtime_without_arg(self):
|
def test_localtime_without_arg(self):
|
||||||
lt0 = time.localtime()
|
lt0 = time.localtime()
|
||||||
lt1 = time.localtime(None)
|
lt1 = time.localtime(None)
|
||||||
t0 = time.mktime(lt0)
|
t0 = time.mktime(lt0)
|
||||||
t1 = time.mktime(lt1)
|
t1 = time.mktime(lt1)
|
||||||
self.assertTrue(0 <= (t1-t0) < 0.2)
|
self.assertAlmostEqual(t1, t0, delta=0.2)
|
||||||
|
|
||||||
class TestLocale(unittest.TestCase):
|
class TestLocale(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -274,16 +277,18 @@ class TestAccept2Year(unittest.TestCase):
|
||||||
time.accept2dyear = self.saved_accept2dyear
|
time.accept2dyear = self.saved_accept2dyear
|
||||||
|
|
||||||
def yearstr(self, y):
|
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):
|
def test_2dyear(self):
|
||||||
self.assertEqual(self.yearstr(0), '2000')
|
with support.check_warnings():
|
||||||
self.assertEqual(self.yearstr(69), '1969')
|
self.assertEqual(self.yearstr(0), '2000')
|
||||||
self.assertEqual(self.yearstr(68), '2068')
|
self.assertEqual(self.yearstr(69), '1969')
|
||||||
self.assertEqual(self.yearstr(99), '1999')
|
self.assertEqual(self.yearstr(68), '2068')
|
||||||
|
self.assertEqual(self.yearstr(99), '1999')
|
||||||
|
|
||||||
def test_invalid(self):
|
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, 100)
|
||||||
self.assertRaises(ValueError, self.yearstr, -1)
|
self.assertRaises(ValueError, self.yearstr, -1)
|
||||||
|
|
||||||
|
@ -293,10 +298,15 @@ class TestAccept2YearBool(TestAccept2Year):
|
||||||
class TestDontAccept2Year(TestAccept2Year):
|
class TestDontAccept2Year(TestAccept2Year):
|
||||||
accept2dyear = 0
|
accept2dyear = 0
|
||||||
def test_2dyear(self):
|
def test_2dyear(self):
|
||||||
self.assertRaises(ValueError, self.yearstr, 0)
|
self.assertEqual(self.yearstr(0), '0')
|
||||||
self.assertRaises(ValueError, self.yearstr, 69)
|
self.assertEqual(self.yearstr(69), '69')
|
||||||
self.assertRaises(ValueError, self.yearstr, 68)
|
self.assertEqual(self.yearstr(68), '68')
|
||||||
self.assertRaises(ValueError, self.yearstr, 99)
|
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):
|
class TestDontAccept2YearBool(TestDontAccept2Year):
|
||||||
accept2dyear = False
|
accept2dyear = False
|
||||||
|
|
|
@ -36,6 +36,14 @@ Core and Builtins
|
||||||
Library
|
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
|
- Issue #7858: Raise an error properly when os.utime() fails under Windows
|
||||||
on an existing file.
|
on an existing file.
|
||||||
|
|
||||||
|
|
|
@ -312,34 +312,42 @@ gettmarg(PyObject *args, struct tm *p)
|
||||||
&p->tm_wday, &p->tm_yday, &p->tm_isdst))
|
&p->tm_wday, &p->tm_yday, &p->tm_isdst))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
/* XXX: Why 1900? If the goal is to interpret 2-digit years as those in
|
/* If year is specified with less than 4 digits, its interpretation
|
||||||
* 20th / 21st century according to the POSIX standard, we can just treat
|
* depends on the accept2dyear value.
|
||||||
* 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 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,
|
PyObject *accept = PyDict_GetItemString(moddict,
|
||||||
"accept2dyear");
|
"accept2dyear");
|
||||||
int acceptval = accept != NULL && PyObject_IsTrue(accept);
|
int acceptval = accept != NULL && PyObject_IsTrue(accept);
|
||||||
if (acceptval == -1)
|
if (acceptval == -1)
|
||||||
return 0;
|
return 0;
|
||||||
if (acceptval) {
|
if (acceptval) {
|
||||||
if (69 <= y && y <= 99)
|
if (0 <= y && y < 69)
|
||||||
y += 1900;
|
|
||||||
else if (0 <= y && y <= 68)
|
|
||||||
y += 2000;
|
y += 2000;
|
||||||
|
else if (69 <= y && y < 100)
|
||||||
|
y += 1900;
|
||||||
else {
|
else {
|
||||||
PyErr_SetString(PyExc_ValueError,
|
PyErr_SetString(PyExc_ValueError,
|
||||||
"year out of range");
|
"year out of range");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
if (PyErr_WarnEx(PyExc_DeprecationWarning,
|
||||||
/* XXX: When accept2dyear is false, we don't have to reject y < 1900.
|
"Century info guessed for a 2-digit year.", 1) != 0)
|
||||||
* Consider removing the following else-clause. */
|
return 0;
|
||||||
else {
|
|
||||||
PyErr_SetString(PyExc_ValueError,
|
|
||||||
"year out of range");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p->tm_year = y - 1900;
|
p->tm_year = y - 1900;
|
||||||
|
@ -462,6 +470,15 @@ time_strftime(PyObject *self, PyObject *args)
|
||||||
else if (!gettmarg(tup, &buf) || !checktm(&buf))
|
else if (!gettmarg(tup, &buf) || !checktm(&buf))
|
||||||
return NULL;
|
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
|
/* Normalize tm_isdst just in case someone foolishly implements %Z
|
||||||
based on the assumption that tm_isdst falls within the range of
|
based on the assumption that tm_isdst falls within the range of
|
||||||
[-1, 1] */
|
[-1, 1] */
|
||||||
|
|
Loading…
Reference in New Issue