Skip to content

Commit

Permalink
api: extract interval encode/decode from class
Browse files Browse the repository at this point in the history
Extract tarantool.Interval encode and decode to external functions. This
is a breaking change, but since there is no tagged release with Interval
yet and API was more internal rather than public, it shouldn't be an
issue.

Follows #229
  • Loading branch information
DifferentialOrange committed Oct 27, 2022
1 parent 1f84255 commit db9e5b8
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 172 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use git version to set package version (#238).
- Extract tarantool.Datetime encode and decode to external
functions (PR #252).
- Extract tarantool.Interval encode and decode to external
functions (PR #252).

### Fixed
- Package build (#238).
Expand Down
103 changes: 96 additions & 7 deletions tarantool/msgpack_ext/interval.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
"""
Tarantool `datetime.interval`_ extension type support module.
Refer to :mod:`~tarantool.msgpack_ext.types.interval`.
The interval MessagePack representation looks like this:
.. code-block:: text
+--------+-------------------------+-------------+----------------+
| MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval |
+--------+-------------------------+-------------+----------------+
Packed interval consists of:
* Packed number of non-zero fields.
* Packed non-null fields.
Each packed field has the following structure:
.. code-block:: text
+----------+=====================+
| field ID | field value |
+----------+=====================+
The number of defined (non-null) fields can be zero. In this case,
the packed interval will be encoded as integer 0.
List of the field IDs:
* 0 – year
* 1 – month
* 2 – week
* 3 – day
* 4 – hour
* 5 – minute
* 6 – second
* 7 – nanosecond
* 8 – adjust
.. _datetime.interval: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type
"""

from tarantool.msgpack_ext.types.interval import Interval
import msgpack

from tarantool.error import MsgpackError

from tarantool.msgpack_ext.types.interval import Interval, Adjust, id_map

EXT_ID = 6
"""
Expand All @@ -22,11 +60,25 @@ def encode(obj):
:return: Encoded interval.
:rtype: :obj:`bytes`
:raise: :exc:`tarantool.Interval.msgpack_encode` exceptions
"""

return obj.msgpack_encode()
buf = bytes()

count = 0
for field_id in id_map.keys():
field_name = id_map[field_id]
value = getattr(obj, field_name)

if field_name == 'adjust':
value = value.value

if value != 0:
buf = buf + msgpack.packb(field_id) + msgpack.packb(value)
count = count + 1

buf = msgpack.packb(count) + buf

return buf

def decode(data):
"""
Expand All @@ -38,7 +90,44 @@ def decode(data):
:return: Decoded interval.
:rtype: :class:`tarantool.Interval`
:raise: :exc:`tarantool.Interval` exceptions
:raise: :exc:`MsgpackError`
"""

return Interval(data)
# If MessagePack data does not contain a field value, it is zero.
# If built not from MessagePack data, set argument values later.
kwargs = {
'year': 0,
'month': 0,
'week': 0,
'day': 0,
'hour': 0,
'minute': 0,
'sec': 0,
'nsec': 0,
'adjust': Adjust(0),
}

if len(data) != 0:
# To create an unpacker is the only way to parse
# a sequence of values in Python msgpack module.
unpacker = msgpack.Unpacker()
unpacker.feed(data)
field_count = unpacker.unpack()
for _ in range(field_count):
field_id = unpacker.unpack()
value = unpacker.unpack()

if field_id not in id_map:
raise MsgpackError(f'Unknown interval field id {field_id}')

field_name = id_map[field_id]

if field_name == 'adjust':
try:
value = Adjust(value)
except ValueError as e:
raise MsgpackError(e)

kwargs[id_map[field_id]] = value

return Interval(**kwargs)
136 changes: 12 additions & 124 deletions tarantool/msgpack_ext/types/interval.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,9 @@
"""
Tarantool `datetime.interval`_ extension type support module.
The interval MessagePack representation looks like this:
.. code-block:: text
+--------+-------------------------+-------------+----------------+
| MP_EXT | Size of packed interval | MP_INTERVAL | PackedInterval |
+--------+-------------------------+-------------+----------------+
Packed interval consists of:
* Packed number of non-zero fields.
* Packed non-null fields.
Each packed field has the following structure:
.. code-block:: text
+----------+=====================+
| field ID | field value |
+----------+=====================+
The number of defined (non-null) fields can be zero. In this case,
the packed interval will be encoded as integer 0.
List of the field IDs:
* 0 – year
* 1 – month
* 2 – week
* 3 – day
* 4 – hour
* 5 – minute
* 6 – second
* 7 – nanosecond
* 8 – adjust
Tarantool `datetime.interval`_ extension type implementation module.
"""

import msgpack
from enum import Enum

from tarantool.error import MsgpackError

id_map = {
0: 'year',
1: 'month',
Expand Down Expand Up @@ -97,14 +58,10 @@ class Interval():
.. _datetime.interval: https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-interval-type
"""

def __init__(self, data=None, *, year=0, month=0, week=0,
def __init__(self, *, year=0, month=0, week=0,
day=0, hour=0, minute=0, sec=0,
nsec=0, adjust=Adjust.NONE):
"""
:param data: MessagePack binary data to decode. If provided,
all other parameters are ignored.
:type data: :obj:`bytes`, optional
:param year: Interval year value.
:type year: :obj:`int`, optional
Expand Down Expand Up @@ -132,61 +89,17 @@ def __init__(self, data=None, *, 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`
"""

# If MessagePack data does not contain a field value, it is zero.
# If built not from MessagePack data, set argument values later.
self.year = 0
self.month = 0
self.week = 0
self.day = 0
self.hour = 0
self.minute = 0
self.sec = 0
self.nsec = 0
self.adjust = Adjust(0)

if data is not None:
if not isinstance(data, bytes):
raise ValueError('data argument (first positional argument) ' +
'expected to be a "bytes" instance')

if len(data) == 0:
return

# To create an unpacker is the only way to parse
# a sequence of values in Python msgpack module.
unpacker = msgpack.Unpacker()
unpacker.feed(data)
field_count = unpacker.unpack()
for _ in range(field_count):
field_id = unpacker.unpack()
value = unpacker.unpack()

if field_id not in id_map:
raise MsgpackError(f'Unknown interval field id {field_id}')

field_name = id_map[field_id]

if field_name == 'adjust':
try:
value = Adjust(value)
except ValueError as e:
raise MsgpackError(e)

setattr(self, id_map[field_id], value)
else:
self.year = year
self.month = month
self.week = week
self.day = day
self.hour = hour
self.minute = minute
self.sec = sec
self.nsec = nsec
self.adjust = adjust

self.year = year
self.month = month
self.week = week
self.day = day
self.hour = hour
self.minute = minute
self.sec = sec
self.nsec = nsec
self.adjust = adjust

def __add__(self, other):
"""
Expand Down Expand Up @@ -319,28 +232,3 @@ def __repr__(self):
f'nsec={self.nsec}, adjust={self.adjust})'

__str__ = __repr__

def msgpack_encode(self):
"""
Encode an interval object.
:rtype: :obj:`bytes`
"""

buf = bytes()

count = 0
for field_id in id_map.keys():
field_name = id_map[field_id]
value = getattr(self, field_name)

if field_name == 'adjust':
value = value.value

if value != 0:
buf = buf + msgpack.packb(field_id) + msgpack.packb(value)
count = count + 1

buf = msgpack.packb(count) + buf

return buf
43 changes: 2 additions & 41 deletions test/suites/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,50 +57,11 @@ def setUp(self):

self.adm("box.space['test']:truncate()")

def test_Interval_bytes_init(self):
dt = tarantool.Interval(b'\x02\x00\x01\x08\x01')

self.assertEqual(dt.year, 1)
self.assertEqual(dt.month, 0)
self.assertEqual(dt.day, 0)
self.assertEqual(dt.hour, 0)
self.assertEqual(dt.minute, 0)
self.assertEqual(dt.sec, 0)
self.assertEqual(dt.nsec, 0)
self.assertEqual(dt.adjust, tarantool.IntervalAdjust.NONE)

def test_Interval_non_bytes_positional_init(self):
def test_Interval_positional_init(self):
self.assertRaisesRegex(
ValueError, re.escape('data argument (first positional argument) ' +
'expected to be a "bytes" instance'),
TypeError, re.escape('__init__() takes 1 positional argument but 2 were given'),
lambda: tarantool.Interval(1))

def test_Interval_bytes_init_ignore_other_fields(self):
dt = tarantool.Interval(b'\x02\x00\x01\x08\x01',
year=2, month=2, day=3, hour=1, minute=2,
sec=3000, nsec=10000000,
adjust=tarantool.IntervalAdjust.LAST)

self.assertEqual(dt.year, 1)
self.assertEqual(dt.month, 0)
self.assertEqual(dt.day, 0)
self.assertEqual(dt.hour, 0)
self.assertEqual(dt.minute, 0)
self.assertEqual(dt.sec, 0)
self.assertEqual(dt.nsec, 0)
self.assertEqual(dt.adjust, tarantool.IntervalAdjust.NONE)

def test_Interval_bytes_init_unknown_field(self):
self.assertRaisesRegex(
MsgpackError, 'Unknown interval field id 9',
lambda: tarantool.Interval(b'\x01\x09\xce\x00\x98\x96\x80'))

def test_Interval_bytes_init_unknown_adjust(self):
self.assertRaisesRegex(
MsgpackError, '3 is not a valid Adjust',
lambda: tarantool.Interval(b'\x02\x07\xce\x00\x98\x96\x80\x08\x03'))


cases = {
'year': {
'python': tarantool.Interval(year=1),
Expand Down

0 comments on commit db9e5b8

Please sign in to comment.