From 0f7f91b57516c56c8e883d1ce9a5b6397a052a95 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 19 Sep 2023 18:58:46 +0300 Subject: [PATCH] datetime: support big values for some platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this patch, tarantool.Datetime constructor used datetime.fromtimestamp function to build a new datetime [1], except for negative timestamps for Windows platform. This constructor branch is used on each Tarantool datetime encoding or while building a tarantool.Datetime object from timestamp. datetime.fromtimestamp have some drawbacks: it "may raise OverflowError, if the timestamp is out of the range of values supported by the platform C localtime() or gmtime() functions, and OSError on localtime() or gmtime() failure. It’s common for this to be restricted to years in 1970 through 2038.". It had never happened on supported Unix platforms, but seem to be an issue for Windows ones. We already workaround this issue for years smaller than 1970 on Windows. After this patch, this workaround will be used for all platforms and timestamp values, allowing to provide similar behavior for platforms both restricted to years in 1970 through 2038 with localtime() or gmtime() or not. 1. https://docs.python.org/3/library/datetime.html#datetime.datetime.fromtimestamp --- CHANGELOG.md | 4 +++ tarantool/msgpack_ext/types/datetime.py | 14 ++++----- test/suites/test_datetime.py | 40 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8555c6ac..a4b400fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Exception rethrow in crud API (PR #310). +- Work with timestamps larger than year 2038 for some platforms (like Windows) (PR #311). + It covers + - building new `tarantool.Datetime` objects from timestamp, + - parsing datetime objects received from Tarantool. ## 1.1.1 - 2023-07-19 diff --git a/tarantool/msgpack_ext/types/datetime.py b/tarantool/msgpack_ext/types/datetime.py index 1a546cfc..993df404 100644 --- a/tarantool/msgpack_ext/types/datetime.py +++ b/tarantool/msgpack_ext/types/datetime.py @@ -6,7 +6,6 @@ from calendar import monthrange from copy import deepcopy from datetime import datetime, timedelta -import sys import pytz @@ -314,13 +313,12 @@ def __init__(self, *, timestamp=None, year=None, month=None, timestamp += nsec // NSEC_IN_SEC nsec = nsec % NSEC_IN_SEC - if (sys.platform.startswith("win")) and (timestamp < 0): - # Fails to create a datetime from negative timestamp on Windows. - _datetime = _EPOCH + timedelta(seconds=timestamp) - else: - # Timezone-naive datetime objects are treated by many datetime methods - # as local times, so we represent time in UTC explicitly if not provided. - _datetime = datetime.fromtimestamp(timestamp, pytz.UTC) + # datetime.fromtimestamp may raise OverflowError, if the timestamp + # is out of the range of values supported by the platform C localtime() + # function, and OSError on localtime() failure. It’s common for this + # to be restricted to years from 1970 through 2038, yet we want + # to support a wider range. + _datetime = _EPOCH + timedelta(seconds=timestamp) if nsec is not None: _datetime = _datetime.replace(microsecond=nsec // NSEC_IN_MKSEC) diff --git a/test/suites/test_datetime.py b/test/suites/test_datetime.py index 9f250804..80958fdc 100644 --- a/test/suites/test_datetime.py +++ b/test/suites/test_datetime.py @@ -153,6 +153,24 @@ def test_datetime_class_api_wth_tz(self): 'type': ValueError, 'msg': 'Failed to create datetime with ambiguous timezone "AET"' }, + 'under_min_timestamp_1': { + 'args': [], + 'kwargs': {'timestamp': -62135596801}, + 'type': OverflowError, + 'msg': 'date value out of range' + }, + 'under_min_timestamp_2': { + 'args': [], + 'kwargs': {'timestamp': -62135596800, 'nsec': -1}, + 'type': OverflowError, + 'msg': 'date value out of range' + }, + 'over_max_timestamp': { + 'args': [], + 'kwargs': {'timestamp': 253402300800}, + 'type': OverflowError, + 'msg': 'date value out of range' + }, } def test_datetime_class_invalid_init(self): @@ -293,6 +311,28 @@ def test_datetime_class_invalid_init(self): 'tarantool': r"datetime.new({timestamp=1661969274, nsec=308543321, " r"tz='Europe/Moscow'})", }, + 'min_datetime': { # Python datetime.MINYEAR is 1. + 'python': tarantool.Datetime(year=1, month=1, day=1, hour=0, minute=0, sec=0), + 'msgpack': (b'\x00\x09\x6e\x88\xf1\xff\xff\xff'), + 'tarantool': r"datetime.new({year=1, month=1, day=1, hour=0, min=0, sec=0})", + }, + 'max_datetime': { # Python datetime.MAXYEAR is 9999. + 'python': tarantool.Datetime(year=9999, month=12, day=31, hour=23, minute=59, sec=59, + nsec=999999999), + 'msgpack': (b'\x7f\x41\xf4\xff\x3a\x00\x00\x00\xff\xc9\x9a\x3b\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({year=9999, month=12, day=31, hour=23, min=59, sec=59," + r"nsec=999999999})", + }, + 'min_datetime_timestamp': { # Python datetime.MINYEAR is 1. + 'python': tarantool.Datetime(timestamp=-62135596800), + 'msgpack': (b'\x00\x09\x6e\x88\xf1\xff\xff\xff'), + 'tarantool': r"datetime.new({timestamp=-62135596800})", + }, + 'max_datetime_timestamp': { # Python datetime.MAXYEAR is 9999. + 'python': tarantool.Datetime(timestamp=253402300799, nsec=999999999), + 'msgpack': (b'\x7f\x41\xf4\xff\x3a\x00\x00\x00\xff\xc9\x9a\x3b\x00\x00\x00\x00'), + 'tarantool': r"datetime.new({timestamp=253402300799, nsec=999999999})", + }, } def test_msgpack_decode(self):