From 862fdd312c3d1fa81bd6ec4370bcc0b713309cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=92=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D1=88=D0=BA=D0=B8=D0=BD?= Date: Mon, 22 Jun 2026 19:52:47 +0300 Subject: [PATCH 1/5] readthedocs: bump build.os to ubuntu-24.04 --- .readthedocs.yaml | 2 +- CHANGELOG.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index bc990993..7561bbea 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,7 @@ version: 2 build: - os: "ubuntu-20.04" + os: "ubuntu-24.04" tools: python: "3.10" jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8edd47e7..fd52d301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Drop Python 3.6 support (PR #327). +### Fixed +- Bump readthedocs build os version to ubuntu-24.04 (PR #336). + Ubuntu-20.04 is no longer supported. + ## 1.2.0 - 2024-03-27 ### Added From c3b3e229f857626a53512573b4f9920604d586a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=92=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D1=88=D0=BA=D0=B8=D0=BD?= Date: Wed, 24 Jun 2026 22:55:35 +0300 Subject: [PATCH 2/5] requirements-test: set upper bound for version of setuptools --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 0f2ad987..ba964e70 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -8,4 +8,4 @@ flake8 == 6.1.0 ; python_version >= '3.8' flake8 == 5.0.4 ; python_version < '3.8' codespell == 2.3.0 ; python_version >= '3.8' codespell == 2.2.5 ; python_version < '3.8' -setuptools >= 75.3.2 +setuptools >= 75.3.2, < 82.0.0 From 2ff426f826274165eb760bdc383230c8873639ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=92=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D1=88=D0=BA=D0=B8=D0=BD?= Date: Sat, 27 Jun 2026 00:07:34 +0300 Subject: [PATCH 3/5] msgpack: support new decimal precision --- CHANGELOG.md | 4 +- tarantool/msgpack_ext/datetime.py | 4 +- tarantool/msgpack_ext/decimal.py | 74 ++++-- tarantool/msgpack_ext/error.py | 4 +- tarantool/msgpack_ext/interval.py | 4 +- tarantool/msgpack_ext/packer.py | 9 +- tarantool/msgpack_ext/unpacker.py | 7 +- tarantool/msgpack_ext/uuid.py | 4 +- tarantool/request.py | 2 +- test/suites/lib/skip.py | 62 +++++ test/suites/test_connection.py | 2 +- test/suites/test_decimal.py | 403 ++++++++++++++++++++++++------ 12 files changed, 467 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd52d301..c68f5f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Drop Python 3.6 support (PR #327). +- Support the maximum decimal precision for tarantool 3.5 and above (PR #342). ### Fixed -- Bump readthedocs build os version to ubuntu-24.04 (PR #336). +- Set upper bound for version of setuptools (PR #342). +- Bump readthedocs build os version to ubuntu-24.04 (PR #342). Ubuntu-20.04 is no longer supported. ## 1.2.0 - 2024-03-27 diff --git a/tarantool/msgpack_ext/datetime.py b/tarantool/msgpack_ext/datetime.py index f7323c5f..6e1c7706 100644 --- a/tarantool/msgpack_ext/datetime.py +++ b/tarantool/msgpack_ext/datetime.py @@ -78,7 +78,7 @@ def get_int_as_bytes(data, size): return data.to_bytes(size, byteorder=BYTEORDER, signed=True) -def encode(obj, _): +def encode(obj, _packer, _tarantool_version): """ Encode a datetime object. @@ -135,7 +135,7 @@ def get_bytes_as_int(data, cursor, size): return int.from_bytes(part, BYTEORDER, signed=True), cursor + size -def decode(data, _): +def decode(data, _unpacker, _tarantool_version): """ Decode a datetime object. diff --git a/tarantool/msgpack_ext/decimal.py b/tarantool/msgpack_ext/decimal.py index f1654784..67972b3d 100644 --- a/tarantool/msgpack_ext/decimal.py +++ b/tarantool/msgpack_ext/decimal.py @@ -59,6 +59,7 @@ import msgpack from tarantool.error import MsgpackError, MsgpackWarning, warn +from tarantool.utils import version_id EXT_ID = 1 """ @@ -66,6 +67,38 @@ """ TARANTOOL_DECIMAL_MAX_DIGITS = 38 +TARANTOOL_DECIMAL_MAX_DIGITS_V35 = 76 +TARANTOOL_DECIMAL_76_DIGITS_VERSION = version_id(3, 5, 0) + + +def get_tarantool_decimal_max_digits(tarantool_version=None): + """ + Get max decimal precision supported by Tarantool version. + + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + + :rtype: :obj:`int` + """ + + if tarantool_version is not None \ + and tarantool_version >= TARANTOOL_DECIMAL_76_DIGITS_VERSION: + return TARANTOOL_DECIMAL_MAX_DIGITS_V35 + + return TARANTOOL_DECIMAL_MAX_DIGITS + + +def decimal_max_digits_errmsg(max_digits): + """ + Build an error / warning message for max decimal precision. + + :param max_digits: Max supported precision. + :type max_digits: :obj:`int` + + :rtype: :obj:`str` + """ + + return f'Tarantool decimal supports a maximum of {max_digits} digits.' def get_mp_sign(sign): @@ -114,7 +147,7 @@ def add_mp_digit(digit, bytes_reverted, digit_count): bytes_reverted.append(digit) -def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind): +def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_version=None): """ Decimal numbers have 38 digits of precision, that is, the total number of digits before and after the decimal point can be 38. If @@ -170,6 +203,9 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind): representation. :type first_digit_ind: :obj:`int` + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + :return: ``True``, if decimal can be encoded to Tarantool decimal without precision loss. ``False`` otherwise. :rtype: :obj:`bool` @@ -179,31 +215,33 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind): :meta private: """ + max_digits = get_tarantool_decimal_max_digits(tarantool_version) + if scale > 0: digit_count = len(str_repr) - 1 - first_digit_ind else: digit_count = len(str_repr) - first_digit_ind - if digit_count <= TARANTOOL_DECIMAL_MAX_DIGITS: + if digit_count <= max_digits: return True - if (digit_count - scale) > TARANTOOL_DECIMAL_MAX_DIGITS: - raise MsgpackError('Decimal cannot be encoded: Tarantool decimal ' - 'supports a maximum of 38 digits.') + if (digit_count - scale) > max_digits: + raise MsgpackError('Decimal cannot be encoded: ' + + decimal_max_digits_errmsg(max_digits)) starts_with_zero = str_repr[first_digit_ind] == '0' - if (digit_count > TARANTOOL_DECIMAL_MAX_DIGITS + 1) or \ - (digit_count == TARANTOOL_DECIMAL_MAX_DIGITS + 1 and not starts_with_zero): + if (digit_count > max_digits + 1) or \ + (digit_count == max_digits + 1 and not starts_with_zero): warn('Decimal encoded with loss of precision: ' - 'Tarantool decimal supports a maximum of 38 digits.', + + decimal_max_digits_errmsg(max_digits), MsgpackWarning) return False return True -def strip_decimal_str(str_repr, scale, first_digit_ind): +def strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version=None): """ Strip decimal digits after the decimal point if decimal cannot be represented as Tarantool decimal without precision loss. @@ -218,12 +256,17 @@ def strip_decimal_str(str_repr, scale, first_digit_ind): representation. :type first_digit_ind: :obj:`int` + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + :meta private: """ + max_digits = get_tarantool_decimal_max_digits(tarantool_version) + assert scale > 0 # Strip extra bytes - str_repr = str_repr[:TARANTOOL_DECIMAL_MAX_DIGITS + 1 + first_digit_ind] + str_repr = str_repr[:max_digits + 1 + first_digit_ind] str_repr = str_repr.rstrip('0') str_repr = str_repr.rstrip('.') @@ -231,13 +274,16 @@ def strip_decimal_str(str_repr, scale, first_digit_ind): return str_repr -def encode(obj, _): +def encode(obj, _packer, tarantool_version=None): """ Encode a decimal object. :param obj: Decimal to encode. :type obj: :obj:`decimal.Decimal` + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + :return: Encoded decimal. :rtype: :obj:`bytes` @@ -262,8 +308,8 @@ def encode(obj, _): sign = '+' first_digit_ind = 0 - if not check_valid_tarantool_decimal(str_repr, scale, first_digit_ind): - str_repr = strip_decimal_str(str_repr, scale, first_digit_ind) + if not check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_version): + str_repr = strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version) bytes_reverted.append(get_mp_sign(sign)) @@ -342,7 +388,7 @@ def add_str_digit(digit, digits_reverted, scale): digits_reverted.append(str(digit)) -def decode(data, _): +def decode(data, _unpacker, _tarantool_version): """ Decode a decimal object. diff --git a/tarantool/msgpack_ext/error.py b/tarantool/msgpack_ext/error.py index ef322d58..724fdd79 100644 --- a/tarantool/msgpack_ext/error.py +++ b/tarantool/msgpack_ext/error.py @@ -17,7 +17,7 @@ """ -def encode(obj, packer): +def encode(obj, packer, _tarantool_version): """ Encode an error object. @@ -35,7 +35,7 @@ def encode(obj, packer): return packer.pack(err_map) -def decode(data, unpacker): +def decode(data, unpacker, _tarantool_version): """ Decode an error object. diff --git a/tarantool/msgpack_ext/interval.py b/tarantool/msgpack_ext/interval.py index 12136772..e9d2e06c 100644 --- a/tarantool/msgpack_ext/interval.py +++ b/tarantool/msgpack_ext/interval.py @@ -52,7 +52,7 @@ """ -def encode(obj, _): +def encode(obj, _packer, _tarantool_version): """ Encode an interval object. @@ -81,7 +81,7 @@ def encode(obj, _): return buf -def decode(data, unpacker): +def decode(data, unpacker, _tarantool_version): """ Decode an interval object. diff --git a/tarantool/msgpack_ext/packer.py b/tarantool/msgpack_ext/packer.py index e2c03b8b..1ef26485 100644 --- a/tarantool/msgpack_ext/packer.py +++ b/tarantool/msgpack_ext/packer.py @@ -28,7 +28,7 @@ ] -def default(obj, packer=None): +def default(obj, packer=None, tarantool_version=None): """ :class:`msgpack.Packer` encoder. @@ -41,6 +41,9 @@ def default(obj, packer=None): (like dictionary in extended error payload) :type packer: :class:`msgpack.Packer`, optional + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + :return: Encoded value. :rtype: :class:`msgpack.ExtType` @@ -49,5 +52,7 @@ def default(obj, packer=None): for encoder in encoders: if isinstance(obj, encoder['type']): - return ExtType(encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer)) + return ExtType( + encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer, tarantool_version), + ) raise TypeError(f"Unknown type: {repr(obj)}") diff --git a/tarantool/msgpack_ext/unpacker.py b/tarantool/msgpack_ext/unpacker.py index 985653d5..c5273f2d 100644 --- a/tarantool/msgpack_ext/unpacker.py +++ b/tarantool/msgpack_ext/unpacker.py @@ -20,7 +20,7 @@ } -def ext_hook(code, data, unpacker=None): +def ext_hook(code, data, unpacker=None, tarantool_version=None): """ :class:`msgpack.Unpacker` decoder. @@ -34,6 +34,9 @@ def ext_hook(code, data, unpacker=None): (like dictionary in extended error payload) :type unpacker: :class:`msgpack.Unpacker`, optional + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + :return: Decoded value. :rtype: :class:`decimal.Decimal` or :class:`uuid.UUID` or or :class:`tarantool.BoxError` or :class:`tarantool.Datetime` @@ -43,5 +46,5 @@ def ext_hook(code, data, unpacker=None): """ if code in decoders: - return decoders[code](data, unpacker) + return decoders[code](data, unpacker, tarantool_version) raise NotImplementedError(f"Unknown msgpack extension type code {code}") diff --git a/tarantool/msgpack_ext/uuid.py b/tarantool/msgpack_ext/uuid.py index 859d096c..b28655af 100644 --- a/tarantool/msgpack_ext/uuid.py +++ b/tarantool/msgpack_ext/uuid.py @@ -21,7 +21,7 @@ """ -def encode(obj, _): +def encode(obj, _packer, _tarantool_version): """ Encode an UUID object. @@ -35,7 +35,7 @@ def encode(obj, _): return obj.bytes -def decode(data, _): +def decode(data, _unpacker, _tarantool_version): """ Decode an UUID object. diff --git a/tarantool/request.py b/tarantool/request.py index 95164b79..cf965736 100644 --- a/tarantool/request.py +++ b/tarantool/request.py @@ -112,7 +112,7 @@ def packer_factory(conn): # inside extension type packers. def default(obj): packer_no_ext = msgpack.Packer(**packer_kwargs) - return packer_default(obj, packer_no_ext) + return packer_default(obj, packer_no_ext, conn.version_id) packer_kwargs['default'] = default return msgpack.Packer(**packer_kwargs) diff --git a/test/suites/lib/skip.py b/test/suites/lib/skip.py index 625caf6a..baf24bee 100644 --- a/test/suites/lib/skip.py +++ b/test/suites/lib/skip.py @@ -80,6 +80,44 @@ def skip_or_run_test_tarantool_call(self, required_tt_version, msg): skip_or_run_test_tarantool_impl(self, required_tt_version, msg) +def skip_or_run_test_tarantool_lt_impl(self, unsupported_tt_version, msg): + """ + Helper to skip or run tests depending on the Tarantool + version. + + Skip tests when the Tarantool version is greater than or equal + to the provided version. + """ + fetch_tarantool_version(self) + + unsupported_version = pkg_resources.parse_version(unsupported_tt_version) + + if self.tnt_version >= unsupported_version: + self.skipTest(f'Tarantool {self.tnt_version} {msg}') + + +def skip_or_run_test_tarantool_lt(func, unsupported_tt_version, msg): + """ + Decorator to skip or run tests depending on the tarantool + version. + + Skip tests when the Tarantool version is greater than or equal + to the provided version. + """ + + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + if func.__name__ == 'setUp': + func(self, *args, **kwargs) + + skip_or_run_test_tarantool_lt_impl(self, unsupported_tt_version, msg) + + if func.__name__ != 'setUp': + func(self, *args, **kwargs) + + return wrapper + + def skip_or_run_test_pcall_require(func, required_tt_module, msg): """ Decorator to skip or run tests depending on tarantool @@ -179,6 +217,30 @@ def skip_or_run_decimal_test(func): 'does not support decimal type') +def skip_or_run_decimal_before_3_5_test(func): + """ + Decorator to skip or run decimal-related tests depending on + the tarantool version. + + Skips tests on Tarantool 3.5.0 and newer. + """ + + return skip_or_run_test_tarantool_lt(func, '3.5.0', + 'supports 76-digit decimals') + + +def skip_or_run_decimal_3_5_test(func): + """ + Decorator to skip or run decimal-related tests depending on + the tarantool version. + + Skips tests on Tarantool older than 3.5.0. + """ + + return skip_or_run_test_tarantool(func, '3.5.0', + 'does not support 76-digit decimals') + + def skip_or_run_uuid_test(func): """ Decorator to skip or run UUID-related tests depending on diff --git a/test/suites/test_connection.py b/test/suites/test_connection.py index 4402f0b0..582fbbfb 100644 --- a/test/suites/test_connection.py +++ b/test/suites/test_connection.py @@ -113,7 +113,7 @@ def my_packer_factory(_): def test_custom_unpacker(self): def my_ext_type_decoder(code, data): if code == ext_decimal.EXT_ID: - return ext_decimal.decode(data, None) - 1 + return ext_decimal.decode(data, None, None) - 1 raise NotImplementedError(f"Unknown msgpack extension type code {code}") def my_unpacker_factory(_): diff --git a/test/suites/test_decimal.py b/test/suites/test_decimal.py index 2875a7da..8a4044ed 100644 --- a/test/suites/test_decimal.py +++ b/test/suites/test_decimal.py @@ -13,13 +13,20 @@ from tarantool.error import MsgpackError, MsgpackWarning from tarantool.msgpack_ext.packer import default as packer_default from tarantool.msgpack_ext.unpacker import ext_hook as unpacker_ext_hook +from tarantool.utils import version_id from .lib.tarantool_server import TarantoolServer -from .lib.skip import skip_or_run_decimal_test +from .lib.skip import ( + skip_or_run_decimal_test, + skip_or_run_decimal_before_3_5_test, + skip_or_run_decimal_3_5_test, +) from .utils import assert_admin_success class TestSuiteDecimal(unittest.TestCase): + decimal_v35_version_id = version_id(3, 5, 0) + @classmethod def setUpClass(cls): print(' DECIMAL EXT TYPE '.center(70, '='), file=sys.stderr) @@ -70,6 +77,22 @@ def setUp(self): """) assert_admin_success(resp) + def assert_decimal_tuple_equals(self, name, tarantool_decimal): + lua_eval = f""" + local tuple = box.space['test']:get('{name}') + assert(tuple ~= nil) + + local dec = {tarantool_decimal} + if tuple[2] == dec then + return true + else + return nil, ('%s is not equal to expected %s'):format( + tostring(tuple[2]), tostring(dec)) + end + """ + + self.assertSequenceEqual(self.con.eval(lua_eval), [True]) + valid_cases = { 'simple_decimal_1': { 'python': decimal.Decimal('0.7'), @@ -172,58 +195,58 @@ def setUp(self): 'tarantool': "decimal.new('0.001')", }, 'decimal_limits_1': { - 'python': decimal.Decimal('11111111111111111111111111111111111111'), + 'python': decimal.Decimal('1' * 38), 'msgpack': (b'\x00\x01\x11\x11\x11\x11\x11\x11\x11\x11\x11' b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x1c'), - 'tarantool': "decimal.new('11111111111111111111111111111111111111')", + 'tarantool': "decimal.new('" + '1' * 38 + "')", }, 'decimal_limits_2': { - 'python': decimal.Decimal('-11111111111111111111111111111111111111'), + 'python': decimal.Decimal('-' + '1' * 38), 'msgpack': (b'\x00\x01\x11\x11\x11\x11\x11\x11\x11\x11\x11' b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x1d'), - 'tarantool': "decimal.new('-11111111111111111111111111111111111111')", + 'tarantool': "decimal.new('-" + '1' * 38 + "')", }, 'decimal_limits_3': { - 'python': decimal.Decimal('0.0000000000000000000000000000000000001'), + 'python': decimal.Decimal('0.' + '0' * 36 + '1'), 'msgpack': (b'\x25\x1c'), - 'tarantool': "decimal.new('0.0000000000000000000000000000000000001')", + 'tarantool': "decimal.new('0." + '0' * 36 + "1')", }, 'decimal_limits_4': { - 'python': decimal.Decimal('-0.0000000000000000000000000000000000001'), + 'python': decimal.Decimal('-0.' + '0' * 36 + '1'), 'msgpack': (b'\x25\x1d'), - 'tarantool': "decimal.new('-0.0000000000000000000000000000000000001')", + 'tarantool': "decimal.new('-0." + '0' * 36 + "1')", }, 'decimal_limits_5': { - 'python': decimal.Decimal('0.00000000000000000000000000000000000001'), + 'python': decimal.Decimal('0.' + '0' * 37 + '1'), 'msgpack': (b'\x26\x1c'), - 'tarantool': "decimal.new('0.00000000000000000000000000000000000001')", + 'tarantool': "decimal.new('0." + '0' * 37 + "1')", }, 'decimal_limits_6': { - 'python': decimal.Decimal('-0.00000000000000000000000000000000000001'), + 'python': decimal.Decimal('-0.' + '0' * 37 + '1'), 'msgpack': (b'\x26\x1d'), - 'tarantool': "decimal.new('-0.00000000000000000000000000000000000001')", + 'tarantool': "decimal.new('-0." + '0' * 37 + "1')", }, 'decimal_limits_7': { - 'python': decimal.Decimal('0.00000000000000000000000000000000000009'), + 'python': decimal.Decimal('0.' + '0' * 37 + '9'), 'msgpack': (b'\x26\x9c'), - 'tarantool': "decimal.new('0.00000000000000000000000000000000000009')", + 'tarantool': "decimal.new('0." + '0' * 37 + "9')", }, 'decimal_limits_8': { - 'python': decimal.Decimal('0.00000000000000000000000000000000000009'), + 'python': decimal.Decimal('0.' + '0' * 37 + '9'), 'msgpack': (b'\x26\x9c'), - 'tarantool': "decimal.new('0.00000000000000000000000000000000000009')", + 'tarantool': "decimal.new('0." + '0' * 37 + "9')", }, 'decimal_limits_9': { - 'python': decimal.Decimal('99999999999999999999999999999999999999'), + 'python': decimal.Decimal('9' * 38), 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9c'), - 'tarantool': "decimal.new('99999999999999999999999999999999999999')", + 'tarantool': "decimal.new('" + '9' * 38 + "')", }, 'decimal_limits_10': { - 'python': decimal.Decimal('-99999999999999999999999999999999999999'), + 'python': decimal.Decimal('-' + '9' * 38), 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9d'), - 'tarantool': "decimal.new('-99999999999999999999999999999999999999')", + 'tarantool': "decimal.new('-" + '9' * 38 + "')", }, 'decimal_limits_11': { 'python': decimal.Decimal('1234567891234567890.0987654321987654321'), @@ -272,12 +295,112 @@ def setUp(self): }, } + v35_valid_cases = { + 'decimal_v35_limits_1': { + 'python': decimal.Decimal('1' * 76), + 'msgpack': (b'\x00\x01\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x1c'), + 'tarantool': "decimal.new('" + '1' * 76 + "')", + }, + 'decimal_v35_limits_2': { + 'python': decimal.Decimal('-' + '1' * 76), + 'msgpack': (b'\x00\x01\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x1d'), + 'tarantool': "decimal.new('-" + '1' * 76 + "')", + }, + 'decimal_v35_limits_3': { + 'python': decimal.Decimal('9' * 76), + 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x9c'), + 'tarantool': "decimal.new('" + '9' * 76 + "')", + }, + 'decimal_v35_limits_4': { + 'python': decimal.Decimal('-' + '9' * 76), + 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x9d'), + 'tarantool': "decimal.new('-" + '9' * 76 + "')", + }, + 'decimal_v35_limits_5': { + 'python': decimal.Decimal('0.' + '0' * 75 + '1'), + 'msgpack': (b'\x4c\x1c'), + 'tarantool': "decimal.new('0." + '0' * 75 + "1')", + }, + 'decimal_v35_limits_6': { + 'python': decimal.Decimal('-0.' + '0' * 75 + '1'), + 'msgpack': (b'\x4c\x1d'), + 'tarantool': "decimal.new('-0." + '0' * 75 + "1')", + }, + 'decimal_v35_limits_7': { + 'python': decimal.Decimal('9' * 38 + '.' + '1' * 38), + 'msgpack': (b'\x26\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x91\x11' + b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x1c'), + 'tarantool': "decimal.new('" + '9' * 38 + "." + '1' * 38 + "')", + }, + 'decimal_v35_limits_8': { + 'python': decimal.Decimal('-' + '9' * 38 + '.' + '1' * 38), + 'msgpack': (b'\x26\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x91\x11' + b'\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11' + b'\x11\x11\x11\x11\x11\x11\x1d'), + 'tarantool': "decimal.new('-" + '9' * 38 + "." + '1' * 38 + "')", + }, + 'decimal_v35_exponent_1': { + 'python': decimal.Decimal('1e75'), + 'msgpack': (b'\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x0c'), + 'tarantool': "decimal.new('1e75')", + }, + 'decimal_v35_exponent_2': { + 'python': decimal.Decimal('-1e75'), + 'msgpack': (b'\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x0d'), + 'tarantool': "decimal.new('-1e75')", + }, + 'decimal_v35_exponent_3': { + 'python': decimal.Decimal('1e-75'), + 'msgpack': (b'\x4b\x1c'), + 'tarantool': "decimal.new('1e-75')", + }, + 'decimal_v35_exponent_4': { + 'python': decimal.Decimal('1.2345e75'), + 'msgpack': (b'\x00\x01\x23\x45\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x0c'), + 'tarantool': "decimal.new('1.2345e75')", + }, + 'decimal_v35_exponent_5': { + 'python': decimal.Decimal('1.2345e-71'), + 'msgpack': (b'\x4b\x12\x34\x5c'), + 'tarantool': "decimal.new('1.2345e-71')", + }, + } + def test_msgpack_decode(self): for name, case in self.valid_cases.items(): with self.subTest(msg=name): self.assertEqual(unpacker_ext_hook(1, case['msgpack']), case['python']) + for name, case in self.v35_valid_cases.items(): + with self.subTest(msg=name): + self.assertEqual(unpacker_ext_hook(1, case['msgpack']), + case['python']) + @skip_or_run_decimal_test def test_tarantool_decode(self): for name, case in self.valid_cases.items(): @@ -288,64 +411,92 @@ def test_tarantool_decode(self): self.con.select('test', name), [[name, case['python']]]) + @skip_or_run_decimal_test + @skip_or_run_decimal_3_5_test + def test_tarantool_decode_v35(self): + for name, case in self.v35_valid_cases.items(): + with self.subTest(msg=name): + self.adm(f"box.space['test']:replace{{'{name}', {case['tarantool']}}}") + + self.assertSequenceEqual( + self.con.select('test', name), + [[name, case['python']]]) + def test_msgpack_encode(self): for name, case in self.valid_cases.items(): with self.subTest(msg=name): self.assertEqual(packer_default(case['python']), msgpack.ExtType(code=1, data=case['msgpack'])) + def test_msgpack_encode_v35(self): + for name, case in self.v35_valid_cases.items(): + with self.subTest(msg=name): + self.assertEqual( + packer_default(case['python'], + tarantool_version=self.decimal_v35_version_id), + msgpack.ExtType(code=1, data=case['msgpack'])) + @skip_or_run_decimal_test def test_tarantool_encode(self): for name, case in self.valid_cases.items(): with self.subTest(msg=name): self.con.insert('test', [name, case['python']]) + self.assert_decimal_tuple_equals(name, case['tarantool']) - lua_eval = f""" - local tuple = box.space['test']:get('{name}') - assert(tuple ~= nil) - - local dec = {case['tarantool']} - if tuple[2] == dec then - return true - else - return nil, ('%s is not equal to expected %s'):format( - tostring(tuple[2]), tostring(dec)) - end - """ - - self.assertSequenceEqual(self.con.eval(lua_eval), [True]) + @skip_or_run_decimal_test + @skip_or_run_decimal_3_5_test + def test_tarantool_encode_v35(self): + for name, case in self.v35_valid_cases.items(): + with self.subTest(msg=name): + self.con.insert('test', [name, case['python']]) + self.assert_decimal_tuple_equals(name, case['tarantool']) - error_cases = { + legacy_error_cases = { 'decimal_limit_break_head_1': { - 'python': decimal.Decimal('999999999999999999999999999999999999999'), + 'python': decimal.Decimal('9' * 39), }, 'decimal_limit_break_head_2': { - 'python': decimal.Decimal('-999999999999999999999999999999999999999'), + 'python': decimal.Decimal('-' + '9' * 39), }, 'decimal_limit_break_head_3': { - 'python': decimal.Decimal('999999999999999999900000099999999999999999999'), + 'python': decimal.Decimal('9' * 19 + '0' * 5 + '9' * 20), }, 'decimal_limit_break_head_4': { - 'python': decimal.Decimal('-999999999999999999900000099999999999999999999'), + 'python': decimal.Decimal('-' + '9' * 19 + '0' * 5 + '9' * 20), }, 'decimal_limit_break_head_5': { - 'python': decimal.Decimal('100000000000000000000000000000000000000.1'), + 'python': decimal.Decimal('1' + '0' * 38 + '.1'), }, 'decimal_limit_break_head_6': { - 'python': decimal.Decimal('-100000000000000000000000000000000000000.1'), + 'python': decimal.Decimal('-1' + '0' * 38 + '.1'), }, 'decimal_limit_break_head_7': { - 'python': decimal.Decimal('1000000000000000000011110000000000000000000.1'), + 'python': decimal.Decimal('1' + '0' * 18 + '1' * 4 + '0' * 19 + '.1'), }, 'decimal_limit_break_head_8': { - 'python': decimal.Decimal('-1000000000000000000011110000000000000000000.1'), + 'python': decimal.Decimal('-1' + '0' * 18 + '1' * 4 + '0' * 19 + '.1'), + }, + } + + v35_error_cases = { + 'decimal_v35_limit_break_head_1': { + 'python': decimal.Decimal('9' * 77), + }, + 'decimal_v35_limit_break_head_2': { + 'python': decimal.Decimal('-' + '9' * 77), + }, + 'decimal_v35_limit_break_head_3': { + 'python': decimal.Decimal('1' + '0' * 76 + '.1'), + }, + 'decimal_v35_limit_break_head_4': { + 'python': decimal.Decimal('-1' + '0' * 76 + '.1'), }, } - def test_msgpack_encode_error(self): + def test_msgpack_encode_legacy_error(self): # pylint: disable=cell-var-from-loop - for name, case in self.error_cases.items(): + for name, case in self.legacy_error_cases.items(): with self.subTest(msg=name): msg = 'Decimal cannot be encoded: Tarantool decimal ' + \ 'supports a maximum of 38 digits.' @@ -353,11 +504,26 @@ def test_msgpack_encode_error(self): MsgpackError, msg, lambda: packer_default(case['python'])) + def test_msgpack_encode_v35_error(self): + # pylint: disable=cell-var-from-loop + + for name, case in self.v35_error_cases.items(): + with self.subTest(msg=name): + msg = 'Decimal cannot be encoded: Tarantool decimal ' + \ + 'supports a maximum of 76 digits.' + self.assertRaisesRegex( + MsgpackError, msg, + lambda: packer_default( + case['python'], + tarantool_version=self.decimal_v35_version_id, + )) + @skip_or_run_decimal_test - def test_tarantool_encode_error(self): + @skip_or_run_decimal_before_3_5_test + def test_tarantool_encode_legacy_error(self): # pylint: disable=cell-var-from-loop - for name, case in self.error_cases.items(): + for name, case in self.legacy_error_cases.items(): with self.subTest(msg=name): msg = 'Decimal cannot be encoded: Tarantool decimal ' + \ 'supports a maximum of 38 digits.' @@ -365,69 +531,119 @@ def test_tarantool_encode_error(self): MsgpackError, msg, lambda: self.con.insert('test', [name, case['python']])) - precision_loss_cases = { + @skip_or_run_decimal_test + @skip_or_run_decimal_3_5_test + def test_tarantool_encode_v35_error(self): + # pylint: disable=cell-var-from-loop + + for name, case in self.v35_error_cases.items(): + with self.subTest(msg=name): + msg = 'Decimal cannot be encoded: Tarantool decimal ' + \ + 'supports a maximum of 76 digits.' + self.assertRaisesRegex( + MsgpackError, msg, + lambda: self.con.insert('test', [name, case['python']])) + + legacy_precision_loss_cases = { 'decimal_limit_break_tail_1': { - 'python': decimal.Decimal('1.00000000000000000000000000000000000001'), + 'python': decimal.Decimal('1.' + '0' * 37 + '1'), 'msgpack': (b'\x00\x1c'), 'tarantool': "decimal.new('1')", }, 'decimal_limit_break_tail_2': { - 'python': decimal.Decimal('-1.00000000000000000000000000000000000001'), + 'python': decimal.Decimal('-1.' + '0' * 37 + '1'), 'msgpack': (b'\x00\x1d'), 'tarantool': "decimal.new('-1')", }, 'decimal_limit_break_tail_3': { - 'python': decimal.Decimal('0.000000000000000000000000000000000000001'), + 'python': decimal.Decimal('0.' + '0' * 38 + '1'), 'msgpack': (b'\x00\x0c'), - 'tarantool': "decimal.new('0.000000000000000000000000000000000000001')", + 'tarantool': "decimal.new('0." + '0' * 38 + "1')", }, 'decimal_limit_break_tail_4': { - 'python': decimal.Decimal('-0.000000000000000000000000000000000000001'), + 'python': decimal.Decimal('-0.' + '0' * 38 + '1'), 'msgpack': (b'\x00\x0d'), - 'tarantool': "decimal.new('-0.000000000000000000000000000000000000001')", + 'tarantool': "decimal.new('-0." + '0' * 38 + "1')", }, 'decimal_limit_break_tail_5': { - 'python': decimal.Decimal('9999999.99999900000000000000000000000000000000000001'), + 'python': decimal.Decimal('9999999.999999' + '0' * 38 + '1'), 'msgpack': (b'\x06\x99\x99\x99\x99\x99\x99\x9c'), 'tarantool': "decimal.new('9999999.999999')", }, 'decimal_limit_break_tail_6': { - 'python': decimal.Decimal('-9999999.99999900000000000000000000000000000000000001'), + 'python': decimal.Decimal('-9999999.999999' + '0' * 38 + '1'), 'msgpack': (b'\x06\x99\x99\x99\x99\x99\x99\x9d'), 'tarantool': "decimal.new('-9999999.999999')", }, 'decimal_limit_break_tail_7': { - 'python': decimal.Decimal('99999999999999999999999999999999999999.1'), + 'python': decimal.Decimal('9' * 38 + '.1'), 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9c'), - 'tarantool': "decimal.new('99999999999999999999999999999999999999')", + 'tarantool': "decimal.new('" + '9' * 38 + "')", }, 'decimal_limit_break_tail_8': { - 'python': decimal.Decimal('-99999999999999999999999999999999999999.1'), + 'python': decimal.Decimal('-' + '9' * 38 + '.1'), 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9d'), - 'tarantool': "decimal.new('-99999999999999999999999999999999999999')", + 'tarantool': "decimal.new('-" + '9' * 38 + "')", }, 'decimal_limit_break_tail_9': { - 'python': decimal.Decimal('99999999999999999999999999999999999999.11111111111111' - '11111111111'), + 'python': decimal.Decimal('9' * 38 + '.' + '1' * 25), 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9c'), - 'tarantool': "decimal.new('99999999999999999999999999999999999999')", + 'tarantool': "decimal.new('" + '9' * 38 + "')", }, 'decimal_limit_break_tail_10': { - 'python': decimal.Decimal('-99999999999999999999999999999999999999.11111111111111' - '11111111111'), + 'python': decimal.Decimal('-' + '9' * 38 + '.' + '1' * 25), 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9d'), - 'tarantool': "decimal.new('-99999999999999999999999999999999999999')", + 'tarantool': "decimal.new('-" + '9' * 38 + "')", + }, + } + + v35_precision_loss_cases = { + 'decimal_v35_limit_break_tail_1': { + 'python': decimal.Decimal('1.' + '0' * 75 + '1'), + 'msgpack': (b'\x00\x1c'), + 'tarantool': "decimal.new('1')", + }, + 'decimal_v35_limit_break_tail_2': { + 'python': decimal.Decimal('-1.' + '0' * 75 + '1'), + 'msgpack': (b'\x00\x1d'), + 'tarantool': "decimal.new('-1')", + }, + 'decimal_v35_limit_break_tail_3': { + 'python': decimal.Decimal('0.' + '0' * 76 + '1'), + 'msgpack': (b'\x00\x0c'), + 'tarantool': "decimal.new('0')", + }, + 'decimal_v35_limit_break_tail_4': { + 'python': decimal.Decimal('-0.' + '0' * 76 + '1'), + 'msgpack': (b'\x00\x0d'), + 'tarantool': "decimal.new('-0')", + }, + 'decimal_v35_limit_break_tail_5': { + 'python': decimal.Decimal('9' * 76 + '.1'), + 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x9c'), + 'tarantool': "decimal.new('" + '9' * 76 + "')", + }, + 'decimal_v35_limit_break_tail_6': { + 'python': decimal.Decimal('-' + '9' * 76 + '.1'), + 'msgpack': (b'\x00\x09\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + b'\x99\x99\x99\x99\x99\x99\x9d'), + 'tarantool': "decimal.new('-" + '9' * 76 + "')", }, } - def test_msgpack_encode_with_precision_loss(self): + def test_msgpack_encode_with_legacy_precision_loss(self): # pylint: disable=cell-var-from-loop - for name, case in self.precision_loss_cases.items(): + for name, case in self.legacy_precision_loss_cases.items(): with self.subTest(msg=name): msg = 'Decimal encoded with loss of precision: ' + \ 'Tarantool decimal supports a maximum of 38 digits.' @@ -440,11 +656,31 @@ def test_msgpack_encode_with_precision_loss(self): ) ) + def test_msgpack_encode_with_v35_precision_loss(self): + # pylint: disable=cell-var-from-loop + + for name, case in self.v35_precision_loss_cases.items(): + with self.subTest(msg=name): + msg = 'Decimal encoded with loss of precision: ' + \ + 'Tarantool decimal supports a maximum of 76 digits.' + + self.assertWarnsRegex( + MsgpackWarning, msg, + lambda: self.assertEqual( + packer_default( + case['python'], + tarantool_version=self.decimal_v35_version_id, + ), + msgpack.ExtType(code=1, data=case['msgpack']) + ) + ) + @skip_or_run_decimal_test - def test_tarantool_encode_with_precision_loss(self): + @skip_or_run_decimal_before_3_5_test + def test_tarantool_encode_with_legacy_precision_loss(self): # pylint: disable=cell-var-from-loop - for name, case in self.precision_loss_cases.items(): + for name, case in self.legacy_precision_loss_cases.items(): with self.subTest(msg=name): msg = 'Decimal encoded with loss of precision: ' + \ 'Tarantool decimal supports a maximum of 38 digits.' @@ -452,21 +688,22 @@ def test_tarantool_encode_with_precision_loss(self): self.assertWarnsRegex( MsgpackWarning, msg, lambda: self.con.insert('test', [name, case['python']])) + self.assert_decimal_tuple_equals(name, case['tarantool']) - lua_eval = f""" - local tuple = box.space['test']:get('{name}') - assert(tuple ~= nil) + @skip_or_run_decimal_test + @skip_or_run_decimal_3_5_test + def test_tarantool_encode_with_v35_precision_loss(self): + # pylint: disable=cell-var-from-loop - local dec = {case['tarantool']} - if tuple[2] == dec then - return true - else - return nil, ('%s is not equal to expected %s'):format( - tostring(tuple[2]), tostring(dec)) - end - """ + for name, case in self.v35_precision_loss_cases.items(): + with self.subTest(msg=name): + msg = 'Decimal encoded with loss of precision: ' + \ + 'Tarantool decimal supports a maximum of 76 digits.' - self.assertSequenceEqual(self.con.eval(lua_eval), [True]) + self.assertWarnsRegex( + MsgpackWarning, msg, + lambda: self.con.insert('test', [name, case['python']])) + self.assert_decimal_tuple_equals(name, case['tarantool']) @skip_or_run_decimal_test def test_primary_key(self): From 1a211f4659ca2b23239d3b4b4109363e791e5854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=92=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D1=88=D0=BA=D0=B8=D0=BD?= Date: Mon, 29 Jun 2026 18:45:34 +0300 Subject: [PATCH 4/5] changelog: remove unnecessary information about internal changes --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c68f5f0f..f507f1bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Set upper bound for version of setuptools (PR #342). -- Bump readthedocs build os version to ubuntu-24.04 (PR #342). - Ubuntu-20.04 is no longer supported. ## 1.2.0 - 2024-03-27 From e625b6a66ae5fc117a31390bd346c5e9430ec17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC=20=D0=92=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D1=88=D0=BA=D0=B8=D0=BD?= Date: Thu, 9 Jul 2026 00:39:32 +0300 Subject: [PATCH 5/5] msgpack: support change way to select max_digits --- tarantool/msgpack_ext/datetime.py | 4 +- tarantool/msgpack_ext/decimal.py | 50 +++++-------------- tarantool/msgpack_ext/error.py | 4 +- tarantool/msgpack_ext/extensions.py | 76 +++++++++++++++++++++++++++++ tarantool/msgpack_ext/interval.py | 4 +- tarantool/msgpack_ext/packer.py | 29 ++--------- tarantool/msgpack_ext/unpacker.py | 21 ++------ tarantool/msgpack_ext/uuid.py | 4 +- test/suites/test_connection.py | 4 +- 9 files changed, 106 insertions(+), 90 deletions(-) create mode 100644 tarantool/msgpack_ext/extensions.py diff --git a/tarantool/msgpack_ext/datetime.py b/tarantool/msgpack_ext/datetime.py index 6e1c7706..f7323c5f 100644 --- a/tarantool/msgpack_ext/datetime.py +++ b/tarantool/msgpack_ext/datetime.py @@ -78,7 +78,7 @@ def get_int_as_bytes(data, size): return data.to_bytes(size, byteorder=BYTEORDER, signed=True) -def encode(obj, _packer, _tarantool_version): +def encode(obj, _): """ Encode a datetime object. @@ -135,7 +135,7 @@ def get_bytes_as_int(data, cursor, size): return int.from_bytes(part, BYTEORDER, signed=True), cursor + size -def decode(data, _unpacker, _tarantool_version): +def decode(data, _): """ Decode a datetime object. diff --git a/tarantool/msgpack_ext/decimal.py b/tarantool/msgpack_ext/decimal.py index 67972b3d..c408266a 100644 --- a/tarantool/msgpack_ext/decimal.py +++ b/tarantool/msgpack_ext/decimal.py @@ -59,34 +59,12 @@ import msgpack from tarantool.error import MsgpackError, MsgpackWarning, warn -from tarantool.utils import version_id EXT_ID = 1 """ `decimal`_ type id. """ -TARANTOOL_DECIMAL_MAX_DIGITS = 38 -TARANTOOL_DECIMAL_MAX_DIGITS_V35 = 76 -TARANTOOL_DECIMAL_76_DIGITS_VERSION = version_id(3, 5, 0) - - -def get_tarantool_decimal_max_digits(tarantool_version=None): - """ - Get max decimal precision supported by Tarantool version. - - :param tarantool_version: Tarantool version identifier. - :type tarantool_version: :obj:`int`, optional - - :rtype: :obj:`int` - """ - - if tarantool_version is not None \ - and tarantool_version >= TARANTOOL_DECIMAL_76_DIGITS_VERSION: - return TARANTOOL_DECIMAL_MAX_DIGITS_V35 - - return TARANTOOL_DECIMAL_MAX_DIGITS - def decimal_max_digits_errmsg(max_digits): """ @@ -147,7 +125,7 @@ def add_mp_digit(digit, bytes_reverted, digit_count): bytes_reverted.append(digit) -def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_version=None): +def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, max_digits): """ Decimal numbers have 38 digits of precision, that is, the total number of digits before and after the decimal point can be 38. If @@ -203,8 +181,8 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_ve representation. :type first_digit_ind: :obj:`int` - :param tarantool_version: Tarantool version identifier. - :type tarantool_version: :obj:`int`, optional + :param max_digits: Max supported precision. + :type max_digits: :obj:`int` :return: ``True``, if decimal can be encoded to Tarantool decimal without precision loss. ``False`` otherwise. @@ -215,8 +193,6 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_ve :meta private: """ - max_digits = get_tarantool_decimal_max_digits(tarantool_version) - if scale > 0: digit_count = len(str_repr) - 1 - first_digit_ind else: @@ -241,7 +217,7 @@ def check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_ve return True -def strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version=None): +def strip_decimal_str(str_repr, scale, first_digit_ind, max_digits): """ Strip decimal digits after the decimal point if decimal cannot be represented as Tarantool decimal without precision loss. @@ -256,14 +232,12 @@ def strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version=None): representation. :type first_digit_ind: :obj:`int` - :param tarantool_version: Tarantool version identifier. - :type tarantool_version: :obj:`int`, optional + :param max_digits: Max supported precision. + :type max_digits: :obj:`int` :meta private: """ - max_digits = get_tarantool_decimal_max_digits(tarantool_version) - assert scale > 0 # Strip extra bytes str_repr = str_repr[:max_digits + 1 + first_digit_ind] @@ -274,15 +248,15 @@ def strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version=None): return str_repr -def encode(obj, _packer, tarantool_version=None): +def encode(obj, _packer, max_digits): """ Encode a decimal object. :param obj: Decimal to encode. :type obj: :obj:`decimal.Decimal` - :param tarantool_version: Tarantool version identifier. - :type tarantool_version: :obj:`int`, optional + :param max_digits: Max supported precision. + :type max_digits: :obj:`int` :return: Encoded decimal. :rtype: :obj:`bytes` @@ -308,8 +282,8 @@ def encode(obj, _packer, tarantool_version=None): sign = '+' first_digit_ind = 0 - if not check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, tarantool_version): - str_repr = strip_decimal_str(str_repr, scale, first_digit_ind, tarantool_version) + if not check_valid_tarantool_decimal(str_repr, scale, first_digit_ind, max_digits): + str_repr = strip_decimal_str(str_repr, scale, first_digit_ind, max_digits) bytes_reverted.append(get_mp_sign(sign)) @@ -388,7 +362,7 @@ def add_str_digit(digit, digits_reverted, scale): digits_reverted.append(str(digit)) -def decode(data, _unpacker, _tarantool_version): +def decode(data, _): """ Decode a decimal object. diff --git a/tarantool/msgpack_ext/error.py b/tarantool/msgpack_ext/error.py index 724fdd79..ef322d58 100644 --- a/tarantool/msgpack_ext/error.py +++ b/tarantool/msgpack_ext/error.py @@ -17,7 +17,7 @@ """ -def encode(obj, packer, _tarantool_version): +def encode(obj, packer): """ Encode an error object. @@ -35,7 +35,7 @@ def encode(obj, packer, _tarantool_version): return packer.pack(err_map) -def decode(data, unpacker, _tarantool_version): +def decode(data, unpacker): """ Decode an error object. diff --git a/tarantool/msgpack_ext/extensions.py b/tarantool/msgpack_ext/extensions.py new file mode 100644 index 00000000..8a644bb5 --- /dev/null +++ b/tarantool/msgpack_ext/extensions.py @@ -0,0 +1,76 @@ +""" +This module provides Tarantool `extension`_ types handlers. + +.. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ +""" +# encoding: utf-8 +import functools +from collections import namedtuple + +from decimal import Decimal +from uuid import UUID +from tarantool.types import BoxError +from tarantool.msgpack_ext.types.datetime import Datetime +from tarantool.msgpack_ext.types.interval import Interval + +import tarantool.msgpack_ext.decimal as ext_decimal +import tarantool.msgpack_ext.uuid as ext_uuid +import tarantool.msgpack_ext.error as ext_error +import tarantool.msgpack_ext.datetime as ext_datetime +import tarantool.msgpack_ext.interval as ext_interval +from tarantool.utils import version_id + + +class Extension(namedtuple("Extension", "decode encode type")): + """ + MessagePack extension type handlers. + + :ivar decode: Extension type decoder. + :vartype decode: :obj:`callable` + + :ivar encode: Extension type encoder. + :vartype encode: :obj:`callable` + + :ivar type: Python type represented by the extension. + :vartype type: :obj:`type` + """ + + +TARANTOOL_DECIMAL_MAX_DIGITS = 38 +TARANTOOL_DECIMAL_MAX_DIGITS_V35 = 76 + + +def init_msgpack_extensions(tarantool_version=None): + """ + Initialize MessagePack extension type handlers. + + :param tarantool_version: Tarantool version identifier. + :type tarantool_version: :obj:`int`, optional + + :return: Mapping from Tarantool extension type id to its handlers. + :rtype: :obj:`dict` + """ + + max_digits = TARANTOOL_DECIMAL_MAX_DIGITS + if tarantool_version is not None and tarantool_version >= version_id(3, 5, 0): + max_digits = TARANTOOL_DECIMAL_MAX_DIGITS_V35 + + return { + ext_decimal.EXT_ID: Extension( + ext_decimal.decode, + functools.partial(ext_decimal.encode, max_digits=max_digits), + Decimal, + ), + ext_uuid.EXT_ID: Extension( + ext_uuid.decode, ext_uuid.encode, UUID, + ), + ext_error.EXT_ID: Extension( + ext_error.decode, ext_error.encode, BoxError, + ), + ext_datetime.EXT_ID: Extension( + ext_datetime.decode, ext_datetime.encode, Datetime, + ), + ext_interval.EXT_ID: Extension( + ext_interval.decode, ext_interval.encode, Interval, + ), + } diff --git a/tarantool/msgpack_ext/interval.py b/tarantool/msgpack_ext/interval.py index e9d2e06c..12136772 100644 --- a/tarantool/msgpack_ext/interval.py +++ b/tarantool/msgpack_ext/interval.py @@ -52,7 +52,7 @@ """ -def encode(obj, _packer, _tarantool_version): +def encode(obj, _): """ Encode an interval object. @@ -81,7 +81,7 @@ def encode(obj, _packer, _tarantool_version): return buf -def decode(data, unpacker, _tarantool_version): +def decode(data, unpacker): """ Decode an interval object. diff --git a/tarantool/msgpack_ext/packer.py b/tarantool/msgpack_ext/packer.py index 1ef26485..97b8c409 100644 --- a/tarantool/msgpack_ext/packer.py +++ b/tarantool/msgpack_ext/packer.py @@ -3,29 +3,10 @@ .. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ """ -# pylint: disable=duplicate-code -from decimal import Decimal -from uuid import UUID from msgpack import ExtType -from tarantool.types import BoxError -from tarantool.msgpack_ext.types.datetime import Datetime -from tarantool.msgpack_ext.types.interval import Interval - -import tarantool.msgpack_ext.decimal as ext_decimal -import tarantool.msgpack_ext.uuid as ext_uuid -import tarantool.msgpack_ext.error as ext_error -import tarantool.msgpack_ext.datetime as ext_datetime -import tarantool.msgpack_ext.interval as ext_interval - -encoders = [ - {'type': Decimal, 'ext': ext_decimal}, - {'type': UUID, 'ext': ext_uuid}, - {'type': BoxError, 'ext': ext_error}, - {'type': Datetime, 'ext': ext_datetime}, - {'type': Interval, 'ext': ext_interval}, -] +from tarantool.msgpack_ext.extensions import init_msgpack_extensions def default(obj, packer=None, tarantool_version=None): @@ -50,9 +31,7 @@ def default(obj, packer=None, tarantool_version=None): :raise: :exc:`~TypeError` """ - for encoder in encoders: - if isinstance(obj, encoder['type']): - return ExtType( - encoder['ext'].EXT_ID, encoder['ext'].encode(obj, packer, tarantool_version), - ) + for ext_id, ext in init_msgpack_extensions(tarantool_version).items(): + if isinstance(obj, ext.type): + return ExtType(ext_id, ext.encode(obj, packer)) raise TypeError(f"Unknown type: {repr(obj)}") diff --git a/tarantool/msgpack_ext/unpacker.py b/tarantool/msgpack_ext/unpacker.py index c5273f2d..33e0ffe6 100644 --- a/tarantool/msgpack_ext/unpacker.py +++ b/tarantool/msgpack_ext/unpacker.py @@ -3,21 +3,7 @@ .. _extension: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/ """ -# pylint: disable=duplicate-code - -import tarantool.msgpack_ext.decimal as ext_decimal -import tarantool.msgpack_ext.uuid as ext_uuid -import tarantool.msgpack_ext.error as ext_error -import tarantool.msgpack_ext.datetime as ext_datetime -import tarantool.msgpack_ext.interval as ext_interval - -decoders = { - ext_decimal.EXT_ID: ext_decimal.decode, - ext_uuid.EXT_ID: ext_uuid.decode, - ext_error.EXT_ID: ext_error.decode, - ext_datetime.EXT_ID: ext_datetime.decode, - ext_interval.EXT_ID: ext_interval.decode, -} +from tarantool.msgpack_ext.extensions import init_msgpack_extensions def ext_hook(code, data, unpacker=None, tarantool_version=None): @@ -44,7 +30,8 @@ def ext_hook(code, data, unpacker=None, tarantool_version=None): :raise: :exc:`NotImplementedError` """ + ext = init_msgpack_extensions(tarantool_version) - if code in decoders: - return decoders[code](data, unpacker, tarantool_version) + if code in ext: + return ext[code].decode(data, unpacker) raise NotImplementedError(f"Unknown msgpack extension type code {code}") diff --git a/tarantool/msgpack_ext/uuid.py b/tarantool/msgpack_ext/uuid.py index b28655af..859d096c 100644 --- a/tarantool/msgpack_ext/uuid.py +++ b/tarantool/msgpack_ext/uuid.py @@ -21,7 +21,7 @@ """ -def encode(obj, _packer, _tarantool_version): +def encode(obj, _): """ Encode an UUID object. @@ -35,7 +35,7 @@ def encode(obj, _packer, _tarantool_version): return obj.bytes -def decode(data, _unpacker, _tarantool_version): +def decode(data, _): """ Decode an UUID object. diff --git a/test/suites/test_connection.py b/test/suites/test_connection.py index 582fbbfb..bc66a59f 100644 --- a/test/suites/test_connection.py +++ b/test/suites/test_connection.py @@ -82,7 +82,7 @@ def test_custom_packer(self): def my_ext_type_encoder(obj): if isinstance(obj, decimal.Decimal): obj = obj + 1 - return msgpack.ExtType(ext_decimal.EXT_ID, ext_decimal.encode(obj, None)) + return msgpack.ExtType(ext_decimal.EXT_ID, ext_decimal.encode(obj, None, 35)) raise TypeError(f"Unknown type: {repr(obj)}") def my_packer_factory(_): @@ -113,7 +113,7 @@ def my_packer_factory(_): def test_custom_unpacker(self): def my_ext_type_decoder(code, data): if code == ext_decimal.EXT_ID: - return ext_decimal.decode(data, None, None) - 1 + return ext_decimal.decode(data, None) - 1 raise NotImplementedError(f"Unknown msgpack extension type code {code}") def my_unpacker_factory(_):