From c2ebdc490ca7fe03f18b60f4ee3593f8b2776555 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 13 Jul 2023 19:31:29 +0300 Subject: [PATCH 1/7] doc: add week to interval example --- tarantool/msgpack_ext/types/interval.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tarantool/msgpack_ext/types/interval.py b/tarantool/msgpack_ext/types/interval.py index 10dc4847..c5bec39e 100644 --- a/tarantool/msgpack_ext/types/interval.py +++ b/tarantool/msgpack_ext/types/interval.py @@ -49,8 +49,8 @@ class Interval(): .. code-block:: python - di = tarantool.Interval(year=-1, month=2, day=3, - hour=4, minute=-5, sec=6, + di = tarantool.Interval(year=-1, month=2, week=-3, + day=4, hour=5, minute=-6, sec=7, nsec=308543321, adjust=tarantool.IntervalAdjust.NONE) From bca64f5704ae2fa0c1ae5365dfea3e4bd3961af5 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 13 Jul 2023 19:32:23 +0300 Subject: [PATCH 2/7] test: add week interval cases Add Interval with week encoding test cases and datetime arithmetic week test case. --- test/suites/test_datetime.py | 6 ++++++ test/suites/test_interval.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/test/suites/test_datetime.py b/test/suites/test_datetime.py index 40f84215..ae22dcfc 100644 --- a/test/suites/test_datetime.py +++ b/test/suites/test_datetime.py @@ -480,6 +480,12 @@ def test_tarantool_datetime_subtraction_different_timezones(self): 'res_add': tarantool.Datetime(year=2009, month=3, day=31), 'res_sub': tarantool.Datetime(year=2009, month=1, day=31), }, + 'week': { + 'arg_1': tarantool.Datetime(year=2008, month=2, day=3), + 'arg_2': tarantool.Interval(week=1), + 'res_add': tarantool.Datetime(year=2008, month=2, day=10), + 'res_sub': tarantool.Datetime(year=2008, month=1, day=27), + }, } def test_python_interval_addition(self): diff --git a/test/suites/test_interval.py b/test/suites/test_interval.py index 77f2cf52..1821e901 100644 --- a/test/suites/test_interval.py +++ b/test/suites/test_interval.py @@ -148,6 +148,24 @@ def test_interval_positional_init(self): 'msgpack': (b'\x00'), 'tarantool': r"datetime.interval.new({adjust='excess'})", }, + 'weeks': { + 'python': tarantool.Interval(week=3), + 'msgpack': (b'\x02\x02\x03\x08\x01'), + 'tarantool': r"datetime.interval.new({week=3})", + }, + 'date_with_week': { + 'python': tarantool.Interval(year=1, month=2, week=3, day=4), + 'msgpack': (b'\x05\x00\x01\x01\x02\x02\x03\x03\x04\x08\x01'), + 'tarantool': r"datetime.interval.new({year=1, month=2, week=3, day=4})", + }, + 'datetime_with_week': { + 'python': tarantool.Interval(year=1, month=2, week=3, day=4, hour=1, minute=2, + sec=3000, nsec=10000000), + 'msgpack': (b'\x09\x00\x01\x01\x02\x02\x03\x03\x04\x04\x01\x05\x02\x06\xcd\x0b\xb8' + b'\x07\xce\x00\x98\x96\x80\x08\x01'), + 'tarantool': r"datetime.interval.new({year=1, month=2, week=3, day=4, hour=1, " + r"min=2, sec=3000, nsec=10000000})", + }, } def test_msgpack_decode(self): From 4ad76aa24c81d98d99072fcc2ba361e0d94aee15 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 13 Jul 2023 19:31:17 +0300 Subject: [PATCH 3/7] interval: fix week arithmetic Before this patch, weeks were ignored in Interval addition and subtraction. This patch fixes the issue. --- CHANGELOG.md | 5 +++++ tarantool/msgpack_ext/types/interval.py | 2 ++ test/suites/test_interval.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad980b7..84b8a35b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed +- `tarantool.Interval` arithmetic with weeks + ## 1.1.0 - 2023-06-30 ### Added diff --git a/tarantool/msgpack_ext/types/interval.py b/tarantool/msgpack_ext/types/interval.py index c5bec39e..c1cf7189 100644 --- a/tarantool/msgpack_ext/types/interval.py +++ b/tarantool/msgpack_ext/types/interval.py @@ -145,6 +145,7 @@ def __add__(self, other): return Interval( year=self.year + other.year, month=self.month + other.month, + week=self.week + other.week, day=self.day + other.day, hour=self.hour + other.hour, minute=self.minute + other.minute, @@ -194,6 +195,7 @@ def __sub__(self, other): return Interval( year=self.year - other.year, month=self.month - other.month, + week=self.week - other.week, day=self.day - other.day, hour=self.hour - other.hour, minute=self.minute - other.minute, diff --git a/test/suites/test_interval.py b/test/suites/test_interval.py index 1821e901..8474620a 100644 --- a/test/suites/test_interval.py +++ b/test/suites/test_interval.py @@ -283,6 +283,28 @@ def test_unknown_adjust_decode(self): 'res_add': tarantool.Interval(year=3, adjust=tarantool.IntervalAdjust.LAST), 'res_sub': tarantool.Interval(year=1, adjust=tarantool.IntervalAdjust.LAST), }, + 'weeks': { + 'arg_1': tarantool.Interval(week=2), + 'arg_2': tarantool.Interval(week=1), + 'res_add': tarantool.Interval(week=3), + 'res_sub': tarantool.Interval(week=1), + }, + 'date_with_week': { + 'arg_1': tarantool.Interval(year=1, month=2, week=3, day=4), + 'arg_2': tarantool.Interval(year=4, month=3, week=2, day=1), + 'res_add': tarantool.Interval(year=5, month=5, week=5, day=5), + 'res_sub': tarantool.Interval(year=-3, month=-1, week=1, day=3), + }, + 'datetime_with_week': { + 'arg_1': tarantool.Interval(year=1, month=2, week=3, day=4, hour=1, minute=2, + sec=3000, nsec=10000000), + 'arg_2': tarantool.Interval(year=2, month=1, week=-1, day=31, hour=-3, minute=0, + sec=1000, nsec=9876543), + 'res_add': tarantool.Interval(year=3, month=3, week=2, day=35, hour=-2, minute=2, + sec=4000, nsec=19876543), + 'res_sub': tarantool.Interval(year=-1, month=1, week=4, day=-27, hour=4, minute=2, + sec=2000, nsec=123457), + }, } def test_python_interval_addition(self): From a668059c1f065816cdcaa09bb8df9a04e2908b0d Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Thu, 13 Jul 2023 19:44:57 +0300 Subject: [PATCH 4/7] interval: display weeks in str and repr --- CHANGELOG.md | 1 + tarantool/msgpack_ext/types/interval.py | 4 +-- test/suites/test_interval.py | 38 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b8a35b..35b5dcdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `tarantool.Interval` arithmetic with weeks +- `tarantool.Interval` weeks display in `str()` and `repr()` ## 1.1.0 - 2023-06-30 diff --git a/tarantool/msgpack_ext/types/interval.py b/tarantool/msgpack_ext/types/interval.py index c1cf7189..54551a18 100644 --- a/tarantool/msgpack_ext/types/interval.py +++ b/tarantool/msgpack_ext/types/interval.py @@ -233,8 +233,8 @@ def __eq__(self, other): return True def __repr__(self): - return f'tarantool.Interval(year={self.year}, month={self.month}, day={self.day}, ' + \ - f'hour={self.hour}, minute={self.minute}, sec={self.sec}, ' + \ + return f'tarantool.Interval(year={self.year}, month={self.month}, week={self.week}, ' + \ + f'day={self.day}, hour={self.hour}, minute={self.minute}, sec={self.sec}, ' + \ f'nsec={self.nsec}, adjust={self.adjust})' __str__ = __repr__ diff --git a/test/suites/test_interval.py b/test/suites/test_interval.py index 8474620a..6917b49c 100644 --- a/test/suites/test_interval.py +++ b/test/suites/test_interval.py @@ -71,42 +71,58 @@ def test_interval_positional_init(self): 'python': tarantool.Interval(year=1), 'msgpack': (b'\x02\x00\x01\x08\x01'), 'tarantool': r"datetime.interval.new({year=1})", + 'str': 'tarantool.Interval(year=1, month=0, week=0, day=0, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', }, 'big_year': { 'python': tarantool.Interval(year=1000), 'msgpack': (b'\x02\x00\xcd\x03\xe8\x08\x01'), 'tarantool': r"datetime.interval.new({year=1000})", + 'str': 'tarantool.Interval(year=1000, month=0, week=0, day=0, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', }, 'date': { 'python': tarantool.Interval(year=1, month=2, day=3), 'msgpack': (b'\x04\x00\x01\x01\x02\x03\x03\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=2, day=3})", + 'str': 'tarantool.Interval(year=1, month=2, week=0, day=3, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', }, 'big_month_date': { 'python': tarantool.Interval(year=1, month=100000, day=3), 'msgpack': (b'\x04\x00\x01\x01\xce\x00\x01\x86\xa0\x03\x03\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=100000, day=3})", + 'str': 'tarantool.Interval(year=1, month=100000, week=0, day=3, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', }, 'time': { 'python': tarantool.Interval(hour=1, minute=2, sec=3), 'msgpack': (b'\x04\x04\x01\x05\x02\x06\x03\x08\x01'), 'tarantool': r"datetime.interval.new({hour=1, min=2, sec=3})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=1, ' + 'minute=2, sec=3, nsec=0, adjust=Adjust.NONE)', }, 'big_seconds_time': { 'python': tarantool.Interval(hour=1, minute=2, sec=3000), 'msgpack': (b'\x04\x04\x01\x05\x02\x06\xcd\x0b\xb8\x08\x01'), 'tarantool': r"datetime.interval.new({hour=1, min=2, sec=3000})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=1, ' + 'minute=2, sec=3000, nsec=0, adjust=Adjust.NONE)', }, 'datetime': { 'python': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, sec=3000), 'msgpack': (b'\x07\x00\x01\x01\x02\x03\x03\x04\x01\x05\x02\x06\xcd\x0b\xb8\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=2, day=3, hour=1, " r"min=2, sec=3000})", + 'str': 'tarantool.Interval(year=1, month=2, week=0, day=3, hour=1, ' + 'minute=2, sec=3000, nsec=0, adjust=Adjust.NONE)', }, 'nanoseconds': { 'python': tarantool.Interval(nsec=10000000), 'msgpack': (b'\x02\x07\xce\x00\x98\x96\x80\x08\x01'), 'tarantool': r"datetime.interval.new({nsec=10000000})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + 'minute=0, sec=0, nsec=10000000, adjust=Adjust.NONE)', }, 'datetime_with_nanoseconds': { 'python': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, @@ -115,6 +131,8 @@ def test_interval_positional_init(self): b'\x00\x98\x96\x80\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=2, day=3, hour=1, " r"min=2, sec=3000, nsec=10000000})", + 'str': 'tarantool.Interval(year=1, month=2, week=0, day=3, hour=1, ' + 'minute=2, sec=3000, nsec=10000000, adjust=Adjust.NONE)', }, 'datetime_none_adjust': { 'python': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, @@ -124,6 +142,8 @@ def test_interval_positional_init(self): b'\x00\x98\x96\x80\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=2, day=3, hour=1, " r"min=2, sec=3000, nsec=10000000, adjust='none'})", + 'str': 'tarantool.Interval(year=1, month=2, week=0, day=3, hour=1, ' + 'minute=2, sec=3000, nsec=10000000, adjust=Adjust.NONE)', }, 'datetime_excess_adjust': { 'python': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, @@ -133,6 +153,8 @@ def test_interval_positional_init(self): b'\x00\x98\x96\x80'), 'tarantool': r"datetime.interval.new({year=1, month=2, day=3, hour=1, " r"min=2, sec=3000, nsec=10000000, adjust='excess'})", + 'str': 'tarantool.Interval(year=1, month=2, week=0, day=3, hour=1, ' + 'minute=2, sec=3000, nsec=10000000, adjust=Adjust.EXCESS)', }, 'datetime_last_adjust': { 'python': tarantool.Interval(year=1, month=2, day=3, hour=1, minute=2, @@ -142,21 +164,29 @@ def test_interval_positional_init(self): b'\x00\x98\x96\x80\x08\x02'), 'tarantool': r"datetime.interval.new({year=1, month=2, day=3, hour=1, " r"min=2, sec=3000, nsec=10000000, adjust='last'})", + 'str': 'tarantool.Interval(year=1, month=2, week=0, day=3, hour=1, ' + 'minute=2, sec=3000, nsec=10000000, adjust=Adjust.LAST)', }, 'all_zeroes': { 'python': tarantool.Interval(adjust=tarantool.IntervalAdjust.EXCESS), 'msgpack': (b'\x00'), 'tarantool': r"datetime.interval.new({adjust='excess'})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.EXCESS)', }, 'weeks': { 'python': tarantool.Interval(week=3), 'msgpack': (b'\x02\x02\x03\x08\x01'), 'tarantool': r"datetime.interval.new({week=3})", + 'str': 'tarantool.Interval(year=0, month=0, week=3, day=0, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', }, 'date_with_week': { 'python': tarantool.Interval(year=1, month=2, week=3, day=4), 'msgpack': (b'\x05\x00\x01\x01\x02\x02\x03\x03\x04\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=2, week=3, day=4})", + 'str': 'tarantool.Interval(year=1, month=2, week=3, day=4, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', }, 'datetime_with_week': { 'python': tarantool.Interval(year=1, month=2, week=3, day=4, hour=1, minute=2, @@ -165,6 +195,8 @@ def test_interval_positional_init(self): b'\x07\xce\x00\x98\x96\x80\x08\x01'), 'tarantool': r"datetime.interval.new({year=1, month=2, week=3, day=4, hour=1, " r"min=2, sec=3000, nsec=10000000})", + 'str': 'tarantool.Interval(year=1, month=2, week=3, day=4, hour=1, ' + 'minute=2, sec=3000, nsec=10000000, adjust=Adjust.NONE)', }, } @@ -216,6 +248,12 @@ def test_tarantool_encode(self): self.assertSequenceEqual(self.adm(lua_eval), [True]) + def test_class_string(self): + for name, case in self.cases.items(): + with self.subTest(msg=name): + self.assertEqual(str(case['python']), case['str']) + self.assertEqual(repr(case['python']), case['str']) + def test_unknown_field_decode(self): case = b'\x01\x09\xce\x00\x98\x96\x80' self.assertRaisesRegex( From 8eef642d9ddf520bb0a784472bfc1bb78a7268d3 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Mon, 17 Jul 2023 17:35:14 +0300 Subject: [PATCH 5/7] interval: validate limits Before this patch, any value was allowed for interval attributes. Now we use the same rules as in Tarantool. A couple of issues were met while developing this patch, follow [1, 2] for core updates. 1. https://github.com/tarantool/tarantool/issues/8878 2. https://github.com/tarantool/tarantool/issues/8887 --- CHANGELOG.md | 3 + tarantool/msgpack_ext/types/interval.py | 58 ++++ test/suites/test_interval.py | 369 ++++++++++++++++++++++++ 3 files changed, 430 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35b5dcdc..1e9aff0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed +- Validate `tarantool.Interval` limits with the same rules as in Tarantool. + ### Fixed - `tarantool.Interval` arithmetic with weeks - `tarantool.Interval` weeks display in `str()` and `repr()` diff --git a/tarantool/msgpack_ext/types/interval.py b/tarantool/msgpack_ext/types/interval.py index 54551a18..e44dcce0 100644 --- a/tarantool/msgpack_ext/types/interval.py +++ b/tarantool/msgpack_ext/types/interval.py @@ -16,6 +16,60 @@ 8: 'adjust', } +# https://github.com/tarantool/tarantool/blob/ff57f990f359f6d7866c1947174d8ba0e97b1ea6/src/lua/datetime.lua#L112-L146 +SECS_PER_DAY = 86400 + +MIN_DATE_YEAR = -5879610 +MIN_DATE_MONTH = 6 +MIN_DATE_DAY = 22 +MAX_DATE_YEAR = 5879611 +MAX_DATE_MONTH = 7 +MAX_DATE_DAY = 11 + +AVERAGE_DAYS_YEAR = 365.25 +AVERAGE_WEEK_YEAR = AVERAGE_DAYS_YEAR / 7 +INT_MAX = 2147483647 +MAX_YEAR_RANGE = MAX_DATE_YEAR - MIN_DATE_YEAR +MAX_MONTH_RANGE = MAX_YEAR_RANGE * 12 +MAX_WEEK_RANGE = MAX_YEAR_RANGE * AVERAGE_WEEK_YEAR +MAX_DAY_RANGE = MAX_YEAR_RANGE * AVERAGE_DAYS_YEAR +MAX_HOUR_RANGE = MAX_DAY_RANGE * 24 +MAX_MIN_RANGE = MAX_HOUR_RANGE * 60 +MAX_SEC_RANGE = MAX_DAY_RANGE * SECS_PER_DAY +MAX_NSEC_RANGE = INT_MAX + +max_val = { + 'year': MAX_YEAR_RANGE, + 'month': MAX_MONTH_RANGE, + 'week': MAX_WEEK_RANGE, + 'day': MAX_DAY_RANGE, + 'hour': MAX_HOUR_RANGE, + 'minute': MAX_MIN_RANGE, + 'sec': MAX_SEC_RANGE, + 'nsec': MAX_NSEC_RANGE, +} + + +def verify_range(intv): + """ + Check allowed values. Approach is the same as in tarantool/tarantool. + + :param intv: Raw interval to verify. + :type intv: :class:`~tarantool.Interval` + + :raise: :exc:`ValueError` + + :meta private: + """ + + for field_name, range_max in max_val.items(): + val = getattr(intv, field_name) + # Tarantool implementation has a bug + # https://github.com/tarantool/tarantool/issues/8878 + if (val > range_max) or (val < -range_max): + raise ValueError(f"value {val} of {field_name} is out of " + f"allowed range [{-range_max}, {range_max}]") + # https://github.com/tarantool/c-dt/blob/cec6acebb54d9e73ea0b99c63898732abd7683a6/dt_arithmetic.h#L34 class Adjust(Enum): @@ -92,6 +146,8 @@ def __init__(self, *, year=0, month=0, week=0, :param adjust: Interval adjustment rule. Refer to :meth:`~tarantool.Datetime.__add__`. :type adjust: :class:`~tarantool.IntervalAdjust`, optional + + :raise: :exc:`ValueError` """ self.year = year @@ -104,6 +160,8 @@ def __init__(self, *, year=0, month=0, week=0, self.nsec = nsec self.adjust = adjust + verify_range(self) + def __add__(self, other): """ Valid operations: diff --git a/test/suites/test_interval.py b/test/suites/test_interval.py index 6917b49c..b2726776 100644 --- a/test/suites/test_interval.py +++ b/test/suites/test_interval.py @@ -13,6 +13,16 @@ from tarantool.error import MsgpackError from tarantool.msgpack_ext.packer import default as packer_default from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook +from tarantool.msgpack_ext.types.interval import ( + MAX_YEAR_RANGE, + MAX_MONTH_RANGE, + MAX_WEEK_RANGE, + MAX_DAY_RANGE, + MAX_HOUR_RANGE, + MAX_MIN_RANGE, + MAX_SEC_RANGE, + MAX_NSEC_RANGE, +) from .lib.tarantool_server import TarantoolServer from .lib.skip import skip_or_run_datetime_test @@ -198,6 +208,262 @@ def test_interval_positional_init(self): 'str': 'tarantool.Interval(year=1, month=2, week=3, day=4, hour=1, ' 'minute=2, sec=3000, nsec=10000000, adjust=Adjust.NONE)', }, + 'min_year_interval': { + 'python': tarantool.Interval(year=-int(MAX_YEAR_RANGE)), + 'msgpack': (b'\x02\x00\xd2\xff\x4c\x91\x8b\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{year=-{int(MAX_YEAR_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{year=-{int(MAX_YEAR_RANGE)} + 1}}) - " + r"datetime.interval.new({year=1})", + 'str': f'tarantool.Interval(year=-{int(MAX_YEAR_RANGE)}, month=0, week=0, day=0, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'max_year_interval': { + 'python': tarantool.Interval(year=int(MAX_YEAR_RANGE)), + 'msgpack': (b'\x02\x00\xce\x00\xb3\x6e\x75\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{year={int(MAX_YEAR_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{year={int(MAX_YEAR_RANGE)} - 1}}) + " + r"datetime.interval.new({year=1})", + 'str': f'tarantool.Interval(year={int(MAX_YEAR_RANGE)}, month=0, week=0, day=0, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'min_month_interval': { + 'python': tarantool.Interval(month=-int(MAX_MONTH_RANGE)), + 'msgpack': (b'\x02\x01\xd2\xf7\x96\xd2\x84\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{month=-{int(MAX_MONTH_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{month=-{int(MAX_MONTH_RANGE)} + 1}}) - " + r"datetime.interval.new({month=1})", + 'str': f'tarantool.Interval(year=0, month=-{int(MAX_MONTH_RANGE)}, week=0, day=0, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'max_month_interval': { + 'python': tarantool.Interval(month=int(MAX_MONTH_RANGE)), + 'msgpack': (b'\x02\x01\xce\x08\x69\x2d\x7c\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{month={int(MAX_MONTH_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{month={int(MAX_MONTH_RANGE)} - 1}}) + " + r"datetime.interval.new({month=1})", + 'str': f'tarantool.Interval(year=0, month={int(MAX_MONTH_RANGE)}, week=0, day=0, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'min_week_interval': { + 'python': tarantool.Interval(week=-int(MAX_WEEK_RANGE)), + 'msgpack': (b'\x02\x02\xd2\xdb\x6d\x85\xa8\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{week=-{int(MAX_WEEK_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{week=-{int(MAX_WEEK_RANGE)} + 1}}) - " + r"datetime.interval.new({week=1})", + 'str': f'tarantool.Interval(year=0, month=0, week=-{int(MAX_WEEK_RANGE)}, day=0, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'max_week_interval': { + 'python': tarantool.Interval(week=int(MAX_WEEK_RANGE)), + 'msgpack': (b'\x02\x02\xce\x24\x92\x7a\x58\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{week={int(MAX_WEEK_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{week={int(MAX_WEEK_RANGE)} - 1}}) + " + r"datetime.interval.new({week=1})", + 'str': f'tarantool.Interval(year=0, month=0, week={int(MAX_WEEK_RANGE)}, day=0, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'min_day_interval': { + 'python': tarantool.Interval(day=-int(MAX_DAY_RANGE)), + 'msgpack': (b'\x02\x03\xd3\xff\xff\xff\xfe\xff\xfe\xa7\x92\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{day=-{int(MAX_DAY_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{day=-{int(MAX_DAY_RANGE)} + 1}}) - " + r"datetime.interval.new({day=1})", + 'str': f'tarantool.Interval(year=0, month=0, week=0, day=-{int(MAX_DAY_RANGE)}, ' + 'hour=0, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'max_day_interval': { + 'python': tarantool.Interval(day=int(MAX_DAY_RANGE)), + 'msgpack': (b'\x02\x03\xcf\x00\x00\x00\x01\x00\x01\x58\x6e\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{day={int(MAX_DAY_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{day={int(MAX_DAY_RANGE)} - 1}}) + " + r"datetime.interval.new({day=1})", + 'str': f'tarantool.Interval(year=0, month=0, week=0, day={int(MAX_DAY_RANGE)}, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'min_int32_day_interval': { + 'python': tarantool.Interval(day=-2147483648), + 'msgpack': (b'\x02\x03\xd2\x80\x00\x00\x00\x08\x01'), + 'tarantool': r"datetime.interval.new({day=-2147483648})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=-2147483648, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'max_int32_day_interval': { + 'python': tarantool.Interval(day=2147483647), + 'msgpack': (b'\x02\x03\xce\x7f\xff\xff\xff\x08\x01'), + 'tarantool': r"datetime.interval.new({day=2147483647})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=2147483647, hour=0, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'min_hour_interval': { + 'python': tarantool.Interval(hour=-int(MAX_HOUR_RANGE)), + 'msgpack': (b'\x02\x04\xd3\xff\xff\xff\xe7\xff\xdf\xb5\xaa\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{hour=-{int(MAX_HOUR_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{hour=-{int(MAX_HOUR_RANGE)} + 1}}) - " + r"datetime.interval.new({hour=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, ' + f'hour=-{int(MAX_HOUR_RANGE)}, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'max_hour_interval': { + 'python': tarantool.Interval(hour=int(MAX_HOUR_RANGE)), + 'msgpack': (b'\x02\x04\xcf\x00\x00\x00\x18\x00\x20\x4a\x56\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{hour={int(MAX_HOUR_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{hour={int(MAX_HOUR_RANGE)} - 1}}) + " + r"datetime.interval.new({hour=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, ' + f'hour={int(MAX_HOUR_RANGE)}, minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'min_int32_hour_interval': { + 'python': tarantool.Interval(hour=-2147483648), + 'msgpack': (b'\x02\x04\xd2\x80\x00\x00\x00\x08\x01'), + 'tarantool': r"datetime.interval.new({hour=-2147483648})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=-2147483648, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'max_int32_hour_interval': { + 'python': tarantool.Interval(hour=2147483647), + 'msgpack': (b'\x02\x04\xce\x7f\xff\xff\xff\x08\x01'), + 'tarantool': r"datetime.interval.new({hour=2147483647})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=2147483647, ' + 'minute=0, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'min_minute_interval': { + 'python': tarantool.Interval(minute=-int(MAX_MIN_RANGE)), + 'msgpack': (b'\x02\x05\xd3\xff\xff\xfa\x5f\xf8\x6e\x93\xd8\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{min=-{int(MAX_MIN_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{min=-{int(MAX_MIN_RANGE)} + 1}}) - " + r"datetime.interval.new({min=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + f'minute=-{int(MAX_MIN_RANGE)}, sec=0, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'max_minute_interval': { + 'python': tarantool.Interval(minute=int(MAX_MIN_RANGE)), + 'msgpack': (b'\x02\x05\xcf\x00\x00\x05\xa0\x07\x91\x6c\x28\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{min={int(MAX_MIN_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{min={int(MAX_MIN_RANGE)} - 1}}) + " + r"datetime.interval.new({min=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + f'minute={int(MAX_MIN_RANGE)}, sec=0, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'min_int32_minute_interval': { + 'python': tarantool.Interval(minute=-2147483648), + 'msgpack': (b'\x02\x05\xd2\x80\x00\x00\x00\x08\x01'), + 'tarantool': r"datetime.interval.new({min=-2147483648})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + 'minute=-2147483648, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'max_int32_minute_interval': { + 'python': tarantool.Interval(minute=2147483647), + 'msgpack': (b'\x02\x05\xce\x7f\xff\xff\xff\x08\x01'), + 'tarantool': r"datetime.interval.new({min=2147483647})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + 'minute=2147483647, sec=0, nsec=0, adjust=Adjust.NONE)', + }, + 'min_sec_interval': { + 'python': tarantool.Interval(sec=-int(MAX_SEC_RANGE)), + 'msgpack': (b'\x02\x06\xd3\xff\xfe\xae\x7e\x39\xea\xa6\xa0\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{sec=-{int(MAX_SEC_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{sec=-{int(MAX_SEC_RANGE)} + 1}}) - " + r"datetime.interval.new({sec=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + f'minute=0, sec=-{int(MAX_SEC_RANGE)}, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'max_sec_interval': { + 'python': tarantool.Interval(sec=int(MAX_SEC_RANGE)), + 'msgpack': (b'\x02\x06\xcf\x00\x01\x51\x81\xc6\x15\x59\x60\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{sec={int(MAX_SEC_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{sec={int(MAX_SEC_RANGE)} - 1}}) + " + r"datetime.interval.new({sec=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + f'minute=0, sec={int(MAX_SEC_RANGE)}, nsec=0, adjust=Adjust.NONE)', + 'tarantool_8887_issue': True, + }, + 'min_int32_sec_interval': { + 'python': tarantool.Interval(sec=-2147483648), + 'msgpack': (b'\x02\x06\xd2\x80\x00\x00\x00\x08\x01'), + 'tarantool': r"datetime.interval.new({sec=-2147483648})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + 'minute=0, sec=-2147483648, nsec=0, adjust=Adjust.NONE)', + }, + 'max_int32_sec_interval': { + 'python': tarantool.Interval(sec=2147483647), + 'msgpack': (b'\x02\x06\xce\x7f\xff\xff\xff\x08\x01'), + 'tarantool': r"datetime.interval.new({sec=2147483647})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + 'minute=0, sec=2147483647, nsec=0, adjust=Adjust.NONE)', + }, + 'min_nsec_interval': { + 'python': tarantool.Interval(nsec=-int(MAX_NSEC_RANGE)), + 'msgpack': (b'\x02\x07\xd2\x80\x00\x00\x01\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{nsec=-{int(MAX_NSEC_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{nsec=-{int(MAX_NSEC_RANGE)} + 1}}) - " + r"datetime.interval.new({nsec=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + f'minute=0, sec=0, nsec=-{int(MAX_NSEC_RANGE)}, adjust=Adjust.NONE)', + }, + 'max_nsec_interval': { + 'python': tarantool.Interval(nsec=int(MAX_NSEC_RANGE)), + 'msgpack': (b'\x02\x07\xce\x7f\xff\xff\xff\x08\x01'), + # Reason why Tarantool datetime is so weird here: + # https://github.com/tarantool/tarantool/issues/8878 + # Replace with f"datetime.interval.new({{nsec={int(MAX_NSEC_RANGE)}}}) + # after fix. + 'tarantool': f"datetime.interval.new({{nsec={int(MAX_NSEC_RANGE)} - 1}}) + " + r"datetime.interval.new({nsec=1})", + 'str': 'tarantool.Interval(year=0, month=0, week=0, day=0, hour=0, ' + f'minute=0, sec=0, nsec={int(MAX_NSEC_RANGE)}, adjust=Adjust.NONE)', + }, } def test_msgpack_decode(self): @@ -215,6 +481,9 @@ def test_msgpack_decode(self): def test_tarantool_decode(self): for name, case in self.cases.items(): with self.subTest(msg=name): + if ('tarantool_8887_issue' in case) and (case['tarantool_8887_issue'] is True): + self.skipTest('See https://github.com/tarantool/tarantool/issues/8887') + self.adm(f"box.space['test']:replace{{'{name}', {case['tarantool']}, 'field'}}") self.assertSequenceEqual(self.con.select('test', name), @@ -230,6 +499,9 @@ def test_msgpack_encode(self): def test_tarantool_encode(self): for name, case in self.cases.items(): with self.subTest(msg=name): + if ('tarantool_8887_issue' in case) and (case['tarantool_8887_issue'] is True): + self.skipTest('See https://github.com/tarantool/tarantool/issues/8887') + self.con.insert('test', [name, case['python'], 'field']) lua_eval = f""" @@ -266,6 +538,87 @@ def test_unknown_adjust_decode(self): MsgpackError, '3 is not a valid Adjust', lambda: unpacker_ext_hook(6, case, self.con._unpacker_factory())) + out_of_range_cases = { + 'year_too_small': { + 'kwargs': {'year': -int(MAX_YEAR_RANGE + 1)}, + 'range': MAX_YEAR_RANGE, + }, + 'year_too_large': { + 'kwargs': {'year': int(MAX_YEAR_RANGE + 1)}, + 'range': MAX_YEAR_RANGE, + }, + 'month_too_small': { + 'kwargs': {'month': -int(MAX_MONTH_RANGE + 1)}, + 'range': MAX_MONTH_RANGE, + }, + 'month_too_big': { + 'kwargs': {'month': int(MAX_MONTH_RANGE + 1)}, + 'range': MAX_MONTH_RANGE, + }, + 'week_too_small': { + 'kwargs': {'week': -int(MAX_WEEK_RANGE + 1)}, + 'range': MAX_WEEK_RANGE, + }, + 'week_too_big': { + 'kwargs': {'week': int(MAX_WEEK_RANGE + 1)}, + 'range': MAX_WEEK_RANGE, + }, + 'day_too_small': { + 'kwargs': {'day': -int(MAX_DAY_RANGE + 1)}, + 'range': MAX_DAY_RANGE, + }, + 'day_too_big': { + 'kwargs': {'day': int(MAX_DAY_RANGE + 1)}, + 'range': MAX_DAY_RANGE, + }, + 'hour_too_small': { + 'kwargs': {'hour': -int(MAX_HOUR_RANGE + 1)}, + 'range': MAX_HOUR_RANGE, + }, + 'hour_too_big': { + 'kwargs': {'hour': int(MAX_HOUR_RANGE + 1)}, + 'range': MAX_HOUR_RANGE, + }, + 'minute_too_small': { + 'kwargs': {'minute': -int(MAX_MIN_RANGE + 1)}, + 'range': MAX_MIN_RANGE, + }, + 'minute_too_big': { + 'kwargs': {'minute': int(MAX_MIN_RANGE + 1)}, + 'range': MAX_MIN_RANGE, + }, + 'sec_too_small': { + 'kwargs': {'sec': -int(MAX_SEC_RANGE + 1)}, + 'range': MAX_SEC_RANGE, + }, + 'sec_too_big': { + 'kwargs': {'sec': int(MAX_SEC_RANGE + 1)}, + 'range': MAX_SEC_RANGE, + }, + 'nsec_too_small': { + 'kwargs': {'nsec': -int(MAX_NSEC_RANGE + 1)}, + 'range': MAX_NSEC_RANGE, + }, + 'nsec_too_big': { + 'kwargs': {'nsec': int(MAX_NSEC_RANGE + 1)}, + 'range': MAX_NSEC_RANGE, + }, + } + + def test_out_of_range(self): + # pylint: disable=cell-var-from-loop + + for name, case in self.out_of_range_cases.items(): + with self.subTest(msg=name): + name = next(iter(case['kwargs'])) + val = case['kwargs'][name] + self.assertRaisesRegex( + ValueError, re.escape( + f"value {val} of {name} is out of " + f"allowed range [{-case['range']}, {case['range']}]" + ), + lambda: tarantool.Interval(**case['kwargs'])) + arithmetic_cases = { 'year': { 'arg_1': tarantool.Interval(year=2), @@ -369,6 +722,22 @@ def test_tarantool_interval_subtraction(self): self.assertSequenceEqual(self.con.call('sub', case['arg_1'], case['arg_2']), [case['res_sub']]) + def test_addition_overflow(self): + self.assertRaisesRegex( + ValueError, re.escape( + f"value {int(MAX_YEAR_RANGE) + 1} of year is out of " + f"allowed range [{-MAX_YEAR_RANGE}, {MAX_YEAR_RANGE}]" + ), + lambda: tarantool.Interval(year=int(MAX_YEAR_RANGE)) + tarantool.Interval(year=1)) + + def test_subtraction_overflow(self): + self.assertRaisesRegex( + ValueError, re.escape( + f"value {-int(MAX_YEAR_RANGE) - 1} of year is out of " + f"allowed range [{-MAX_YEAR_RANGE}, {MAX_YEAR_RANGE}]" + ), + lambda: tarantool.Interval(year=-int(MAX_YEAR_RANGE)) - tarantool.Interval(year=1)) + @classmethod def tearDownClass(cls): cls.con.close() From 59e8f9191197938cbba456b30f4ff8bdb05944f3 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 18 Jul 2023 10:20:47 +0300 Subject: [PATCH 6/7] changelog: fix spelling --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e9aff0e..19ea2b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -207,7 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support pandas way to build datetime from timestamp (PR #252). `timestamp_since_utc_epoch` is a parameter to set timestamp - convertion behavior for timezone-aware datetimes. + conversion behavior for timezone-aware datetimes. If ``False`` (default), behaves similar to Tarantool `datetime.new()`: @@ -227,7 +227,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Thus, if ``False``, datetime is computed from timestamp since epoch and then timezone is applied without any - convertion. In that case, `dt.timestamp` won't be equal to + conversion. In that case, `dt.timestamp` won't be equal to initialization `timestamp` for all timezones with non-zero offset. If ``True``, behaves similar to `pandas.Timestamp`: From fe0ca6ff98a0a7bd308045350614e97681587058 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 18 Jul 2023 10:18:51 +0300 Subject: [PATCH 7/7] lint: validate changelog with codespell --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 25809cd8..d3919795 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ install: PYTHON_FILES=tarantool test setup.py docs/source/conf.py -TEXT_FILES=README.rst docs/source/*.rst +TEXT_FILES=README.rst CHANGELOG.md docs/source/*.rst .PHONY: lint lint: python3 -m pylint --recursive=y $(PYTHON_FILES)