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):