Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c64708a

Browse files
committed
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.
1 parent 696efdd commit c64708a

4 files changed

Lines changed: 102 additions & 56 deletions

File tree

Doc/library/time.rst

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,41 @@ An explanation of some terminology and conventions is in order.
2424

2525
.. index:: single: Year 2038
2626

27-
* The functions in this module do not handle dates and times before the epoch or
27+
* The functions in this module may not handle dates and times before the epoch or
2828
far in the future. The cut-off point in the future is determined by the C
29-
library; for Unix, it is typically in 2038.
29+
library; for 32-bit systems, it is typically in 2038.
3030

3131
.. index::
3232
single: Year 2000
3333
single: Y2K
3434

3535
.. _time-y2kissues:
3636

37-
* **Year 2000 (Y2K) issues**: Python depends on the platform's C library, which
37+
* **Year 2000 (Y2K) issues**: Python depends on the platform's C library, which
3838
generally doesn't have year 2000 issues, since all dates and times are
39-
represented internally as seconds since the epoch. Functions accepting a
40-
:class:`struct_time` (see below) generally require a 4-digit year. For backward
41-
compatibility, 2-digit years are supported if the module variable
42-
``accept2dyear`` is a non-zero integer; this variable is initialized to ``1``
43-
unless the environment variable :envvar:`PYTHONY2K` is set to a non-empty
44-
string, in which case it is initialized to ``0``. Thus, you can set
45-
:envvar:`PYTHONY2K` to a non-empty string in the environment to require 4-digit
46-
years for all year input. When 2-digit years are accepted, they are converted
47-
according to the POSIX or X/Open standard: values 69-99 are mapped to 1969-1999,
48-
and values 0--68 are mapped to 2000--2068. Values 100--1899 are always illegal.
49-
Note that this is new as of Python 1.5.2(a2); earlier versions, up to Python
50-
1.5.1 and 1.5.2a1, would add 1900 to year values below 1900.
39+
represented internally as seconds since the epoch. Function :func:`strptime`
40+
can parse 2-digit years when given ``%y`` format code. When 2-digit years are
41+
parsed, they are converted according to the POSIX and ISO C standards: values
42+
69--99 are mapped to 1969--1999, and values 0--68 are mapped to 2000--2068.
43+
44+
For backward compatibility, years with less than 4 digits are treated
45+
specially by :func:`asctime`, :func:`mktime`, and :func:`strftime` functions
46+
that operate on a 9-tuple or :class:`struct_time` values. If year (the first
47+
value in the 9-tuple) is specified with less than 4 digits, its interpretation
48+
depends on the value of ``accept2dyear`` variable.
49+
50+
If ``accept2dyear`` is true (default), a backward compatibility behavior is
51+
invoked as follows:
52+
53+
- for 2-digit year, century is guessed according to POSIX rules for
54+
``%y`` strptime format. A deprecation warning is issued when century
55+
information is guessed in this way.
56+
57+
- for 3-digit or negative year, a :exc:`ValueError` exception is raised.
58+
59+
If ``accept2dyear`` is false (set by the program or as a result of a
60+
non-empty value assigned to ``PYTHONY2K`` environment variable) all year
61+
values are interpreted as given.
5162

5263
.. index::
5364
single: UTC

Lib/test/test_time.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import unittest
44
import locale
55
import sysconfig
6+
import warnings
67

78
class TimeTestCase(unittest.TestCase):
89

@@ -19,10 +20,10 @@ def test_clock(self):
1920
time.clock()
2021

2122
def test_conversions(self):
22-
self.assertTrue(time.ctime(self.t)
23-
== time.asctime(time.localtime(self.t)))
24-
self.assertTrue(int(time.mktime(time.localtime(self.t)))
25-
== int(self.t))
23+
self.assertEqual(time.ctime(self.t),
24+
time.asctime(time.localtime(self.t)))
25+
self.assertEqual(int(time.mktime(time.localtime(self.t))),
26+
int(self.t))
2627

2728
def test_sleep(self):
2829
time.sleep(1.2)
@@ -44,7 +45,7 @@ def _bounds_checking(self, func=time.strftime):
4445

4546
# Check year [1900, max(int)]
4647
self.assertRaises(ValueError, func,
47-
(1899, 1, 1, 0, 0, 0, 0, 1, -1))
48+
(999, 1, 1, 0, 0, 0, 0, 1, -1))
4849
if time.accept2dyear:
4950
self.assertRaises(ValueError, func,
5051
(-1, 1, 1, 0, 0, 0, 0, 1, -1))
@@ -97,7 +98,8 @@ def test_default_values_for_zero(self):
9798
# No test for daylight savings since strftime() does not change output
9899
# based on its value.
99100
expected = "2000 01 01 00 00 00 1 001"
100-
result = time.strftime("%Y %m %d %H %M %S %w %j", (0,)*9)
101+
with support.check_warnings():
102+
result = time.strftime("%Y %m %d %H %M %S %w %j", (0,)*9)
101103
self.assertEqual(expected, result)
102104

103105
def test_strptime(self):
@@ -141,14 +143,15 @@ def test_ctime(self):
141143
self.assertEqual(time.ctime(t), 'Sun Sep 16 01:03:52 1973')
142144
t = time.mktime((2000, 1, 1, 0, 0, 0, 0, 0, -1))
143145
self.assertEqual(time.ctime(t), 'Sat Jan 1 00:00:00 2000')
144-
try:
145-
bigval = time.mktime((10000, 1, 10) + (0,)*6)
146-
except (ValueError, OverflowError):
147-
# If mktime fails, ctime will fail too. This may happen
148-
# on some platforms.
149-
pass
150-
else:
151-
self.assertEqual(time.ctime(bigval)[-5:], '10000')
146+
for year in [-100, 100, 1000, 2000, 10000]:
147+
try:
148+
testval = time.mktime((year, 1, 10) + (0,)*6)
149+
except (ValueError, OverflowError):
150+
# If mktime fails, ctime will fail too. This may happen
151+
# on some platforms.
152+
pass
153+
else:
154+
self.assertEqual(time.ctime(testval)[20:], str(year))
152155

153156
@unittest.skipIf(not hasattr(time, "tzset"),
154157
"time module has no attribute tzset")
@@ -239,14 +242,14 @@ def test_gmtime_without_arg(self):
239242
gt1 = time.gmtime(None)
240243
t0 = time.mktime(gt0)
241244
t1 = time.mktime(gt1)
242-
self.assertTrue(0 <= (t1-t0) < 0.2)
245+
self.assertAlmostEqual(t1, t0, delta=0.2)
243246

244247
def test_localtime_without_arg(self):
245248
lt0 = time.localtime()
246249
lt1 = time.localtime(None)
247250
t0 = time.mktime(lt0)
248251
t1 = time.mktime(lt1)
249-
self.assertTrue(0 <= (t1-t0) < 0.2)
252+
self.assertAlmostEqual(t1, t0, delta=0.2)
250253

251254
class TestLocale(unittest.TestCase):
252255
def setUp(self):
@@ -274,16 +277,18 @@ def tearDown(self):
274277
time.accept2dyear = self.saved_accept2dyear
275278

276279
def yearstr(self, y):
277-
return time.strftime('%Y', (y,) + (0,) * 8)
280+
# return time.strftime('%Y', (y,) + (0,) * 8)
281+
return time.asctime((y,) + (0,) * 8).split()[-1]
278282

279283
def test_2dyear(self):
280-
self.assertEqual(self.yearstr(0), '2000')
281-
self.assertEqual(self.yearstr(69), '1969')
282-
self.assertEqual(self.yearstr(68), '2068')
283-
self.assertEqual(self.yearstr(99), '1999')
284+
with support.check_warnings():
285+
self.assertEqual(self.yearstr(0), '2000')
286+
self.assertEqual(self.yearstr(69), '1969')
287+
self.assertEqual(self.yearstr(68), '2068')
288+
self.assertEqual(self.yearstr(99), '1999')
284289

285290
def test_invalid(self):
286-
self.assertRaises(ValueError, self.yearstr, 1899)
291+
self.assertRaises(ValueError, self.yearstr, 999)
287292
self.assertRaises(ValueError, self.yearstr, 100)
288293
self.assertRaises(ValueError, self.yearstr, -1)
289294

@@ -293,10 +298,15 @@ class TestAccept2YearBool(TestAccept2Year):
293298
class TestDontAccept2Year(TestAccept2Year):
294299
accept2dyear = 0
295300
def test_2dyear(self):
296-
self.assertRaises(ValueError, self.yearstr, 0)
297-
self.assertRaises(ValueError, self.yearstr, 69)
298-
self.assertRaises(ValueError, self.yearstr, 68)
299-
self.assertRaises(ValueError, self.yearstr, 99)
301+
self.assertEqual(self.yearstr(0), '0')
302+
self.assertEqual(self.yearstr(69), '69')
303+
self.assertEqual(self.yearstr(68), '68')
304+
self.assertEqual(self.yearstr(99), '99')
305+
self.assertEqual(self.yearstr(999), '999')
306+
self.assertEqual(self.yearstr(9999), '9999')
307+
308+
def test_invalid(self):
309+
pass
300310

301311
class TestDontAccept2YearBool(TestDontAccept2Year):
302312
accept2dyear = False

Misc/NEWS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ Core and Builtins
3636
Library
3737
-------
3838

39+
- Issue #10827: Changed the rules for 2-digit years. The time.asctime
40+
function will now format any year when ``time.accept2dyear`` is
41+
false and will accept years >= 1000 otherwise. The year range
42+
accepted by ``time.mktime`` and ``time.strftime`` is still system
43+
dependent, but ``time.mktime`` will now accept full range supported
44+
by the OS. Conversion of 2-digit years to 4-digit is deprecated.
45+
46+
3947
- Issue #7858: Raise an error properly when os.utime() fails under Windows
4048
on an existing file.
4149

Modules/timemodule.c

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -312,34 +312,42 @@ gettmarg(PyObject *args, struct tm *p)
312312
&p->tm_wday, &p->tm_yday, &p->tm_isdst))
313313
return 0;
314314

315-
/* XXX: Why 1900? If the goal is to interpret 2-digit years as those in
316-
* 20th / 21st century according to the POSIX standard, we can just treat
317-
* 0 <= y < 100 as special. Year 100 is probably too ambiguous and should
318-
* be rejected, but years 101 through 1899 can be passed through.
315+
/* If year is specified with less than 4 digits, its interpretation
316+
* depends on the accept2dyear value.
317+
*
318+
* If accept2dyear is true (default), a backward compatibility behavior is
319+
* invoked as follows:
320+
*
321+
* - for 2-digit year, century is guessed according to POSIX rules for
322+
* %y strptime format: 21st century for y < 69, 20th century
323+
* otherwise. A deprecation warning is issued when century
324+
* information is guessed in this way.
325+
*
326+
* - for 3-digit or negative year, a ValueError exception is raised.
327+
*
328+
* If accept2dyear is false (set by the program or as a result of a
329+
* non-empty value assigned to PYTHONY2K environment variable) all year
330+
* values are interpreted as given.
319331
*/
320-
if (y < 1900) {
332+
if (y < 1000) {
321333
PyObject *accept = PyDict_GetItemString(moddict,
322334
"accept2dyear");
323335
int acceptval = accept != NULL && PyObject_IsTrue(accept);
324336
if (acceptval == -1)
325337
return 0;
326338
if (acceptval) {
327-
if (69 <= y && y <= 99)
328-
y += 1900;
329-
else if (0 <= y && y <= 68)
339+
if (0 <= y && y < 69)
330340
y += 2000;
341+
else if (69 <= y && y < 100)
342+
y += 1900;
331343
else {
332344
PyErr_SetString(PyExc_ValueError,
333345
"year out of range");
334346
return 0;
335347
}
336-
}
337-
/* XXX: When accept2dyear is false, we don't have to reject y < 1900.
338-
* Consider removing the following else-clause. */
339-
else {
340-
PyErr_SetString(PyExc_ValueError,
341-
"year out of range");
342-
return 0;
348+
if (PyErr_WarnEx(PyExc_DeprecationWarning,
349+
"Century info guessed for a 2-digit year.", 1) != 0)
350+
return 0;
343351
}
344352
}
345353
p->tm_year = y - 1900;
@@ -462,6 +470,15 @@ time_strftime(PyObject *self, PyObject *args)
462470
else if (!gettmarg(tup, &buf) || !checktm(&buf))
463471
return NULL;
464472

473+
/* XXX: Reportedly, some systems have issues formating dates prior to year
474+
* 1900. These systems should be identified and this check should be
475+
* moved to appropriate system specific section below. */
476+
if (buf.tm_year < 0) {
477+
PyErr_Format(PyExc_ValueError, "year=%d is before 1900; "
478+
"the strftime() method requires year >= 1900",
479+
buf.tm_year + 1900);
480+
}
481+
465482
/* Normalize tm_isdst just in case someone foolishly implements %Z
466483
based on the assumption that tm_isdst falls within the range of
467484
[-1, 1] */

0 commit comments

Comments
 (0)