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

Skip to content

Commit 5d272cc

Browse files
committed
Close #14180: Factorize code to convert a number of seconds to time_t, timeval or timespec
time.ctime(), gmtime(), time.localtime(), datetime.date.fromtimestamp(), datetime.datetime.fromtimestamp() and datetime.datetime.utcfromtimestamp() now raises an OverflowError, instead of a ValueError, if the timestamp does not fit in time_t. datetime.datetime.fromtimestamp() and datetime.datetime.utcfromtimestamp() now round microseconds towards zero instead of rounding to nearest with ties going away from zero.
1 parent 3cac309 commit 5d272cc

16 files changed

Lines changed: 277 additions & 221 deletions

File tree

Doc/library/datetime.rst

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,17 @@ Other constructors, all class methods:
395395
.. classmethod:: date.fromtimestamp(timestamp)
396396

397397
Return the local date corresponding to the POSIX timestamp, such as is returned
398-
by :func:`time.time`. This may raise :exc:`ValueError`, if the timestamp is out
398+
by :func:`time.time`. This may raise :exc:`OverflowError`, if the timestamp is out
399399
of the range of values supported by the platform C :c:func:`localtime` function.
400400
It's common for this to be restricted to years from 1970 through 2038. Note
401401
that on non-POSIX systems that include leap seconds in their notion of a
402402
timestamp, leap seconds are ignored by :meth:`fromtimestamp`.
403403

404+
.. versionchanged:: 3.3
405+
Raise :exc:`OverflowError` instead of :exc:`ValueError` if the timestamp
406+
is out of the range of values supported by the platform C
407+
:c:func:`localtime` function.
408+
404409

405410
.. classmethod:: date.fromordinal(ordinal)
406411

@@ -712,6 +717,11 @@ Other constructors, all class methods:
712717
and then it's possible to have two timestamps differing by a second that yield
713718
identical :class:`.datetime` objects. See also :meth:`utcfromtimestamp`.
714719

720+
.. versionchanged:: 3.3
721+
Raise :exc:`OverflowError` instead of :exc:`ValueError` if the timestamp
722+
is out of the range of values supported by the platform C
723+
:c:func:`localtime` or :c:func:`gmtime` functions
724+
715725

716726
.. classmethod:: datetime.utcfromtimestamp(timestamp)
717727

@@ -737,6 +747,11 @@ Other constructors, all class methods:
737747

738748
timestamp = (dt - datetime(1970, 1, 1, tzinfo=timezone.utc)) / timedelta(seconds=1)
739749

750+
.. versionchanged:: 3.3
751+
Raise :exc:`OverflowError` instead of :exc:`ValueError` if the timestamp
752+
is out of the range of values supported by the platform C
753+
:c:func:`gmtime` function.
754+
740755

741756
.. classmethod:: datetime.fromordinal(ordinal)
742757

Include/pytime.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,22 @@ do { \
3939
(tv_end.tv_usec - tv_start.tv_usec) * 0.000001)
4040

4141
#ifndef Py_LIMITED_API
42+
/* Convert a number of seconds, int or float, to time_t. */
43+
PyAPI_FUNC(int) _PyTime_ObjectToTime_t(
44+
PyObject *obj,
45+
time_t *sec);
46+
47+
/* Convert a number of seconds, int or float, to a timeval structure.
48+
usec is in the range [0; 999999] and rounded towards zero.
49+
For example, -1.2 is converted to (-2, 800000). */
50+
PyAPI_FUNC(int) _PyTime_ObjectToTimeval(
51+
PyObject *obj,
52+
time_t *sec,
53+
long *usec);
54+
4255
/* Convert a number of seconds, int or float, to a timespec structure.
43-
nsec is always in the range [0; 999999999]. For example, -1.2 is converted
44-
to (-2, 800000000). */
56+
nsec is in the range [0; 999999999] and rounded towards zero.
57+
For example, -1.2 is converted to (-2, 800000000). */
4558
PyAPI_FUNC(int) _PyTime_ObjectToTimespec(
4659
PyObject *obj,
4760
time_t *sec,

Include/timefuncs.h

Lines changed: 0 additions & 25 deletions
This file was deleted.

Lib/datetime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,7 +1360,7 @@ def fromtimestamp(cls, t, tz=None):
13601360
converter = _time.localtime if tz is None else _time.gmtime
13611361

13621362
t, frac = divmod(t, 1.0)
1363-
us = round(frac * 1e6)
1363+
us = int(frac * 1e6)
13641364

13651365
# If timestamp is less than one microsecond smaller than a
13661366
# full second, us can be rounded up to 1000000. In this case,
@@ -1380,7 +1380,7 @@ def fromtimestamp(cls, t, tz=None):
13801380
def utcfromtimestamp(cls, t):
13811381
"Construct a UTC datetime from a POSIX timestamp (like time.time())."
13821382
t, frac = divmod(t, 1.0)
1383-
us = round(frac * 1e6)
1383+
us = int(frac * 1e6)
13841384

13851385
# If timestamp is less than one microsecond smaller than a
13861386
# full second, us can be rounded up to 1000000. In this case,

Lib/test/datetimetester.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -977,7 +977,7 @@ def test_insane_fromtimestamp(self):
977977
# exempt such platforms (provided they return reasonable
978978
# results!).
979979
for insane in -1e200, 1e200:
980-
self.assertRaises(ValueError, self.theclass.fromtimestamp,
980+
self.assertRaises(OverflowError, self.theclass.fromtimestamp,
981981
insane)
982982

983983
def test_today(self):
@@ -1736,20 +1736,40 @@ def test_utcfromtimestamp(self):
17361736
self.verify_field_equality(expected, got)
17371737

17381738
def test_microsecond_rounding(self):
1739-
# Test whether fromtimestamp "rounds up" floats that are less
1740-
# than 1/2 microsecond smaller than an integer.
17411739
for fts in [self.theclass.fromtimestamp,
17421740
self.theclass.utcfromtimestamp]:
1743-
self.assertEqual(fts(0.9999999), fts(1))
1744-
self.assertEqual(fts(0.99999949).microsecond, 999999)
1741+
zero = fts(0)
1742+
self.assertEqual(zero.second, 0)
1743+
self.assertEqual(zero.microsecond, 0)
1744+
minus_one = fts(-1e-6)
1745+
self.assertEqual(minus_one.second, 59)
1746+
self.assertEqual(minus_one.microsecond, 999999)
1747+
1748+
t = fts(-1e-8)
1749+
self.assertEqual(t, minus_one)
1750+
t = fts(-9e-7)
1751+
self.assertEqual(t, minus_one)
1752+
t = fts(-1e-7)
1753+
self.assertEqual(t, minus_one)
1754+
1755+
t = fts(1e-7)
1756+
self.assertEqual(t, zero)
1757+
t = fts(9e-7)
1758+
self.assertEqual(t, zero)
1759+
t = fts(0.99999949)
1760+
self.assertEqual(t.second, 0)
1761+
self.assertEqual(t.microsecond, 999999)
1762+
t = fts(0.9999999)
1763+
self.assertEqual(t.second, 0)
1764+
self.assertEqual(t.microsecond, 999999)
17451765

17461766
def test_insane_fromtimestamp(self):
17471767
# It's possible that some platform maps time_t to double,
17481768
# and that this test will fail there. This test should
17491769
# exempt such platforms (provided they return reasonable
17501770
# results!).
17511771
for insane in -1e200, 1e200:
1752-
self.assertRaises(ValueError, self.theclass.fromtimestamp,
1772+
self.assertRaises(OverflowError, self.theclass.fromtimestamp,
17531773
insane)
17541774

17551775
def test_insane_utcfromtimestamp(self):
@@ -1758,7 +1778,7 @@ def test_insane_utcfromtimestamp(self):
17581778
# exempt such platforms (provided they return reasonable
17591779
# results!).
17601780
for insane in -1e200, 1e200:
1761-
self.assertRaises(ValueError, self.theclass.utcfromtimestamp,
1781+
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp,
17621782
insane)
17631783
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
17641784
def test_negative_float_fromtimestamp(self):

Lib/test/test_time.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def test_insane_timestamps(self):
281281
# results!).
282282
for func in time.ctime, time.gmtime, time.localtime:
283283
for unreasonable in -1e200, 1e200:
284-
self.assertRaises(ValueError, func, unreasonable)
284+
self.assertRaises(OverflowError, func, unreasonable)
285285

286286
def test_ctime_without_arg(self):
287287
# Not sure how to check the values, since the clock could tick
@@ -365,11 +365,8 @@ def test_localtime_failure(self):
365365
for time_t in (-1, 2**30, 2**33, 2**60):
366366
try:
367367
time.localtime(time_t)
368-
except ValueError as err:
369-
if str(err) == "timestamp out of range for platform time_t":
370-
self.skipTest("need 64-bit time_t")
371-
else:
372-
raise
368+
except OverflowError:
369+
self.skipTest("need 64-bit time_t")
373370
except OSError:
374371
invalid_time_t = time_t
375372
break
@@ -498,19 +495,63 @@ class TestStrftime4dyear(_TestStrftimeYear, _Test4dYear):
498495

499496

500497
class TestPytime(unittest.TestCase):
498+
def setUp(self):
499+
self.invalid_values = (
500+
-(2 ** 100), 2 ** 100,
501+
-(2.0 ** 100.0), 2.0 ** 100.0,
502+
)
503+
504+
def test_time_t(self):
505+
from _testcapi import pytime_object_to_time_t
506+
for obj, time_t in (
507+
(0, 0),
508+
(-1, -1),
509+
(-1.0, -1),
510+
(-1.9, -1),
511+
(1.0, 1),
512+
(1.9, 1),
513+
):
514+
self.assertEqual(pytime_object_to_time_t(obj), time_t)
515+
516+
for invalid in self.invalid_values:
517+
self.assertRaises(OverflowError, pytime_object_to_time_t, invalid)
518+
519+
def test_timeval(self):
520+
from _testcapi import pytime_object_to_timeval
521+
for obj, timeval in (
522+
(0, (0, 0)),
523+
(-1, (-1, 0)),
524+
(-1.0, (-1, 0)),
525+
(1e-6, (0, 1)),
526+
(-1e-6, (-1, 999999)),
527+
(-1.2, (-2, 800000)),
528+
(1.1234560, (1, 123456)),
529+
(1.1234569, (1, 123456)),
530+
(-1.1234560, (-2, 876544)),
531+
(-1.1234561, (-2, 876543)),
532+
):
533+
self.assertEqual(pytime_object_to_timeval(obj), timeval)
534+
535+
for invalid in self.invalid_values:
536+
self.assertRaises(OverflowError, pytime_object_to_timeval, invalid)
537+
501538
def test_timespec(self):
502539
from _testcapi import pytime_object_to_timespec
503540
for obj, timespec in (
504541
(0, (0, 0)),
505542
(-1, (-1, 0)),
506543
(-1.0, (-1, 0)),
544+
(1e-9, (0, 1)),
507545
(-1e-9, (-1, 999999999)),
508546
(-1.2, (-2, 800000000)),
509-
(1.123456789, (1, 123456789)),
547+
(1.1234567890, (1, 123456789)),
548+
(1.1234567899, (1, 123456789)),
549+
(-1.1234567890, (-2, 876543211)),
550+
(-1.1234567891, (-2, 876543210)),
510551
):
511552
self.assertEqual(pytime_object_to_timespec(obj), timespec)
512553

513-
for invalid in (-(2 ** 100), -(2.0 ** 100.0), 2 ** 100, 2.0 ** 100.0):
554+
for invalid in self.invalid_values:
514555
self.assertRaises(OverflowError, pytime_object_to_timespec, invalid)
515556

516557

Misc/NEWS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ Core and Builtins
2424
Library
2525
-------
2626

27+
- Issue #14180: time.ctime(), gmtime(), time.localtime(),
28+
datetime.date.fromtimestamp(), datetime.datetime.fromtimestamp() and
29+
datetime.datetime.utcfromtimestamp() now raises an OverflowError, instead of
30+
a ValueError, if the timestamp does not fit in time_t.
31+
32+
- Issue #14180: datetime.datetime.fromtimestamp() and
33+
datetime.datetime.utcfromtimestamp() now round microseconds towards zero
34+
instead of rounding to nearest with ties going away from zero.
35+
2736
- Issue #10543: Fix unittest test discovery with Jython bytecode files.
2837

2938
- Issue #1178863: Separate initialisation from setting when initializing

Modules/_datetimemodule.c

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77

88
#include <time.h>
99

10-
#include "_time.h"
11-
1210
/* Differentiate between building the core module and building extension
1311
* modules.
1412
*/
@@ -2441,15 +2439,15 @@ date_new(PyTypeObject *type, PyObject *args, PyObject *kw)
24412439

24422440
/* Return new date from localtime(t). */
24432441
static PyObject *
2444-
date_local_from_time_t(PyObject *cls, double ts)
2442+
date_local_from_object(PyObject *cls, PyObject *obj)
24452443
{
24462444
struct tm *tm;
24472445
time_t t;
24482446
PyObject *result = NULL;
24492447

2450-
t = _PyTime_DoubleToTimet(ts);
2451-
if (t == (time_t)-1 && PyErr_Occurred())
2448+
if (_PyTime_ObjectToTime_t(obj, &t) == -1)
24522449
return NULL;
2450+
24532451
tm = localtime(&t);
24542452
if (tm)
24552453
result = PyObject_CallFunction(cls, "iii",
@@ -2494,11 +2492,11 @@ date_today(PyObject *cls, PyObject *dummy)
24942492
static PyObject *
24952493
date_fromtimestamp(PyObject *cls, PyObject *args)
24962494
{
2497-
double timestamp;
2495+
PyObject *timestamp;
24982496
PyObject *result = NULL;
24992497

2500-
if (PyArg_ParseTuple(args, "d:fromtimestamp", &timestamp))
2501-
result = date_local_from_time_t(cls, timestamp);
2498+
if (PyArg_ParseTuple(args, "O:fromtimestamp", &timestamp))
2499+
result = date_local_from_object(cls, timestamp);
25022500
return result;
25032501
}
25042502

@@ -4096,31 +4094,14 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
40964094
* to get that much precision (e.g., C time() isn't good enough).
40974095
*/
40984096
static PyObject *
4099-
datetime_from_timestamp(PyObject *cls, TM_FUNC f, double timestamp,
4097+
datetime_from_timestamp(PyObject *cls, TM_FUNC f, PyObject *timestamp,
41004098
PyObject *tzinfo)
41014099
{
41024100
time_t timet;
4103-
double fraction;
4104-
int us;
4101+
long us;
41054102

4106-
timet = _PyTime_DoubleToTimet(timestamp);
4107-
if (timet == (time_t)-1 && PyErr_Occurred())
4103+
if (_PyTime_ObjectToTimeval(timestamp, &timet, &us) == -1)
41084104
return NULL;
4109-
fraction = timestamp - (double)timet;
4110-
us = (int)round_to_long(fraction * 1e6);
4111-
if (us < 0) {
4112-
/* Truncation towards zero is not what we wanted
4113-
for negative numbers (Python's mod semantics) */
4114-
timet -= 1;
4115-
us += 1000000;
4116-
}
4117-
/* If timestamp is less than one microsecond smaller than a
4118-
* full second, round up. Otherwise, ValueErrors are raised
4119-
* for some floats. */
4120-
if (us == 1000000) {
4121-
timet += 1;
4122-
us = 0;
4123-
}
41244105
return datetime_from_timet_and_us(cls, f, timet, us, tzinfo);
41254106
}
41264107

@@ -4181,11 +4162,11 @@ static PyObject *
41814162
datetime_fromtimestamp(PyObject *cls, PyObject *args, PyObject *kw)
41824163
{
41834164
PyObject *self;
4184-
double timestamp;
4165+
PyObject *timestamp;
41854166
PyObject *tzinfo = Py_None;
41864167
static char *keywords[] = {"timestamp", "tz", NULL};
41874168

4188-
if (! PyArg_ParseTupleAndKeywords(args, kw, "d|O:fromtimestamp",
4169+
if (! PyArg_ParseTupleAndKeywords(args, kw, "O|O:fromtimestamp",
41894170
keywords, &timestamp, &tzinfo))
41904171
return NULL;
41914172
if (check_tzinfo_subclass(tzinfo) < 0)
@@ -4210,10 +4191,10 @@ datetime_fromtimestamp(PyObject *cls, PyObject *args, PyObject *kw)
42104191
static PyObject *
42114192
datetime_utcfromtimestamp(PyObject *cls, PyObject *args)
42124193
{
4213-
double timestamp;
4194+
PyObject *timestamp;
42144195
PyObject *result = NULL;
42154196

4216-
if (PyArg_ParseTuple(args, "d:utcfromtimestamp", &timestamp))
4197+
if (PyArg_ParseTuple(args, "O:utcfromtimestamp", &timestamp))
42174198
result = datetime_from_timestamp(cls, gmtime, timestamp,
42184199
Py_None);
42194200
return result;

0 commit comments

Comments
 (0)