Skip to content

Commit 760e1cf

Browse files
types: support working with binary for Python 3
Before this patch, both bytes and str were encoded as mp_str. It was possible to work with utf and non-utf strings, but not with varbinary [1] (mp_bin). This patch adds varbinary support for Python 3 by default. Python 2 connector behavior remains the same. For encoding="utf-8" (default), the following behavior is expected now: (Python 3 -> Tarantool -> Python 3) bytes -> mp_bin (varbinary) -> bytes str -> mp_str (string) -> str For encoding=None, the following behavior is expected now: (Python 3 -> Tarantool -> Python 3) bytes -> mp_str (string) -> bytes This patch changes current behavior for Python 3. Now bytes objects encoded to varbinary by default. bytes objects are also supported as keys. This patch does not add new restrictions (like "do not permit to use str in encoding=None mode because result may be confusing") to preserve current behavior (for example, using space name as str in schema get_space). 1. tarantool/tarantool#4201 Closes #105
1 parent 0f95f28 commit 760e1cf

File tree

4 files changed

+190
-25
lines changed

4 files changed

+190
-25
lines changed

Diff for: tarantool/request.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Request types definitions
55
'''
66

7+
import sys
78
import collections
89
import msgpack
910
import hashlib
@@ -84,8 +85,13 @@ def __init__(self, conn):
8485
# The option controls whether to pack binary (non-unicode)
8586
# string values as mp_bin or as mp_str.
8687
#
87-
# The default behaviour of the connector is to pack both
88-
# bytes and Unicode strings as mp_str.
88+
# The default behaviour of the Python 2 connector is to pack
89+
# both bytes and Unicode strings as mp_str.
90+
#
91+
# The default behaviour of the Python 3 connector (since
92+
# default encoding is "utf-8") is to pack bytes as mp_bin
93+
# and Unicode strings as mp_str. encoding=None mode must
94+
# be used to work with non-utf strings.
8995
#
9096
# msgpack-0.5.0 (and only this version) warns when the
9197
# option is unset:
@@ -98,7 +104,10 @@ def __init__(self, conn):
98104
# just always set it for all msgpack versions to get rid
99105
# of the warning on msgpack-0.5.0 and to keep our
100106
# behaviour on msgpack-1.0.0.
101-
packer_kwargs['use_bin_type'] = False
107+
if conn.encoding is None or sys.version_info.major == 2:
108+
packer_kwargs['use_bin_type'] = False
109+
else:
110+
packer_kwargs['use_bin_type'] = True
102111

103112
self.packer = msgpack.Packer(**packer_kwargs)
104113

Diff for: tarantool/utils.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
if sys.version_info.major == 2:
77
string_types = (basestring, )
88
integer_types = (int, long)
9+
supported_types = integer_types + string_types + (float,)
10+
911
ENCODING_DEFAULT = None
12+
1013
if sys.version_info.minor < 6:
1114
binary_types = (str, )
1215
else:
@@ -17,10 +20,13 @@ def strxor(rhs, lhs):
1720
return "".join(chr(ord(x) ^ ord(y)) for x, y in zip(rhs, lhs))
1821

1922
elif sys.version_info.major == 3:
20-
binary_types = (bytes, )
21-
string_types = (str, )
22-
integer_types = (int, )
23+
binary_types = (bytes, )
24+
string_types = (str, )
25+
integer_types = (int, )
26+
supported_types = integer_types + string_types + binary_types + (float,)
27+
2328
ENCODING_DEFAULT = "utf-8"
29+
2430
from base64 import decodebytes as base64_decode
2531

2632
def strxor(rhs, lhs):
@@ -43,7 +49,7 @@ def check_key(*args, **kwargs):
4349
elif args[0] is None and kwargs['select']:
4450
return []
4551
for key in args:
46-
assert isinstance(key, integer_types + string_types + (float,))
52+
assert isinstance(key, supported_types)
4753
return list(args)
4854

4955

Diff for: test/suites/lib/skip.py

+65-17
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import functools
22
import pkg_resources
33
import re
4+
import sys
45

5-
SQL_SUPPORT_TNT_VERSION = '2.0.0'
66

7-
8-
def skip_or_run_sql_test(func):
9-
"""Decorator to skip or run SQL-related tests depending on the tarantool
7+
def skip_or_run_test_tarantool(func, REQUIRED_TNT_VERSION, msg):
8+
"""Decorator to skip or run tests depending on the tarantool
109
version.
1110
12-
Tarantool supports SQL-related stuff only since 2.0.0 version. So this
13-
decorator should wrap every SQL-related test to skip it if the tarantool
14-
version < 2.0.0 is used for testing.
15-
16-
Also, it can be used with the 'setUp' method for skipping the whole test
17-
suite.
11+
Also, it can be used with the 'setUp' method for skipping
12+
the whole test suite.
1813
"""
1914

2015
@functools.wraps(func)
@@ -28,16 +23,69 @@ def wrapper(self, *args, **kwargs):
2823
).group()
2924

3025
tnt_version = pkg_resources.parse_version(self.tnt_version)
31-
sql_support_tnt_version = pkg_resources.parse_version(
32-
SQL_SUPPORT_TNT_VERSION
33-
)
26+
support_version = pkg_resources.parse_version(REQUIRED_TNT_VERSION)
3427

35-
if tnt_version < sql_support_tnt_version:
36-
self.skipTest(
37-
'Tarantool %s does not support SQL' % self.tnt_version
38-
)
28+
if tnt_version < support_version:
29+
self.skipTest('Tarantool %s %s' % (self.tnt_version, msg))
3930

4031
if func.__name__ != 'setUp':
4132
func(self, *args, **kwargs)
4233

4334
return wrapper
35+
36+
37+
def skip_or_run_test_python_major(func, REQUIRED_PYTHON_MAJOR, msg):
38+
"""Decorator to skip or run tests depending on the Python major
39+
version.
40+
41+
Also, it can be used with the 'setUp' method for skipping
42+
the whole test suite.
43+
"""
44+
45+
@functools.wraps(func)
46+
def wrapper(self, *args, **kwargs):
47+
if func.__name__ == 'setUp':
48+
func(self, *args, **kwargs)
49+
50+
major = sys.version_info.major
51+
if major != REQUIRED_PYTHON_MAJOR:
52+
self.skipTest('Python %s connector %s' % (major, msg))
53+
54+
if func.__name__ != 'setUp':
55+
func(self, *args, **kwargs)
56+
57+
return wrapper
58+
59+
60+
def skip_or_run_sql_test(func):
61+
"""Decorator to skip or run SQL-related tests depending on the
62+
tarantool version.
63+
64+
Tarantool supports SQL-related stuff only since 2.0.0 version.
65+
So this decorator should wrap every SQL-related test to skip it if
66+
the tarantool version < 2.0.0 is used for testing.
67+
"""
68+
69+
return skip_or_run_test_tarantool(func, '2.0.0', 'does not support SQL')
70+
71+
72+
def skip_or_run_varbinary_test(func):
73+
"""Decorator to skip or run VARBINARY-related tests depending on
74+
the tarantool version.
75+
76+
Tarantool supports VARBINARY type only since 2.2.1 version.
77+
See https://github.com/tarantool/tarantool/issues/4201
78+
"""
79+
80+
return skip_or_run_test_tarantool(func, '2.2.1',
81+
'does not support VARBINARY type')
82+
83+
84+
def skip_or_run_mp_bin_test(func):
85+
"""Decorator to skip or run mp_bin-related tests depending on
86+
the Python version.
87+
88+
Python 2 connector do not support mp_bin.
89+
"""
90+
91+
return skip_or_run_test_python_major(func, 3, 'does not support mp_bin')

Diff for: test/suites/test_dml.py

+103-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import unittest
77
import tarantool
88

9+
from .lib.skip import skip_or_run_mp_bin_test, skip_or_run_varbinary_test
910
from .lib.tarantool_server import TarantoolServer
1011

1112
class TestSuite_Request(unittest.TestCase):
@@ -17,6 +18,14 @@ def setUpClass(self):
1718
self.srv.script = 'test/suites/box.lua'
1819
self.srv.start()
1920
self.con = tarantool.Connection(self.srv.host, self.srv.args['primary'])
21+
self.con_encoding_utf8 = tarantool.Connection(self.srv.host,
22+
self.srv.args['primary'],
23+
encoding='utf-8')
24+
self.con_encoding_none = tarantool.Connection(self.srv.host,
25+
self.srv.args['primary'],
26+
encoding=None)
27+
self.conns = [self.con, self.con_encoding_utf8, self.con_encoding_none]
28+
2029
self.adm = self.srv.admin
2130
self.space_created = self.adm("box.schema.create_space('space_1')")
2231
self.adm("""
@@ -31,17 +40,64 @@ def setUpClass(self):
3140
parts = {2, 'num', 3, 'str'},
3241
unique = false})
3342
""".replace('\n', ' '))
43+
3444
self.space_created = self.adm("box.schema.create_space('space_2')")
3545
self.adm("""
3646
box.space['space_2']:create_index('primary', {
3747
type = 'hash',
3848
parts = {1, 'num'},
3949
unique = true})
4050
""".replace('\n', ' '))
51+
52+
self.adm("box.schema.create_space('space_str')")
53+
self.adm("""
54+
box.space['space_str']:create_index('primary', {
55+
type = 'tree',
56+
parts = {1, 'str'},
57+
unique = true})
58+
""".replace('\n', ' '))
59+
60+
self.adm("box.schema.create_space('space_varbin')")
61+
self.adm("""
62+
box.space['space_varbin']:create_index('primary', {
63+
type = 'tree',
64+
parts = {1, 'varbinary'},
65+
unique = true})
66+
""".replace('\n', ' '))
67+
self.adm("""
68+
buffer = require('buffer')
69+
ffi = require('ffi')
70+
71+
function encode_bin(bytes)
72+
local tmpbuf = buffer.ibuf()
73+
local p = tmpbuf:alloc(3 + #bytes)
74+
p[0] = 0x91
75+
p[1] = 0xC4
76+
p[2] = #bytes
77+
for i, c in pairs(bytes) do
78+
p[i + 3 - 1] = c
79+
end
80+
return tmpbuf
81+
end
82+
83+
function bintuple_insert(space, bytes)
84+
local tmpbuf = encode_bin(bytes)
85+
ffi.cdef[[
86+
int box_insert(uint32_t space_id, const char *tuple, const char *tuple_end, box_tuple_t **result);
87+
]]
88+
ffi.C.box_insert(space.id, tmpbuf.rpos, tmpbuf.wpos, nil)
89+
end
90+
""")
4191
self.adm("json = require('json')")
4292
self.adm("fiber = require('fiber')")
4393
self.adm("uuid = require('uuid')")
4494

95+
def assertNotRaises(self, func, *args, **kwargs):
96+
try:
97+
func(*args, **kwargs)
98+
except Exception as e:
99+
self.fail('Function raised Exception: %s' % repr(e))
100+
45101
def setUp(self):
46102
# prevent a remote tarantool from clean our session
47103
if self.srv.is_started():
@@ -54,7 +110,8 @@ def test_00_00_authenticate(self):
54110
self.assertIsNone(self.srv.admin("""
55111
box.schema.user.grant('test', 'execute,read,write', 'universe')
56112
"""))
57-
self.assertEqual(self.con.authenticate('test', 'test')._data, None)
113+
for con in self.conns:
114+
self.assertEqual(con.authenticate('test', 'test')._data, None)
58115

59116
def test_00_01_space_created(self):
60117
# Check that space is created in setUpClass
@@ -299,6 +356,51 @@ def test_12_update_fields(self):
299356
[[2, 'help', 7]]
300357
)
301358

359+
def test_13_00_string_insert_encoding_utf8_behavior(self):
360+
self.assertNotRaises(
361+
self.con_encoding_utf8.insert,
362+
'space_str', [ 'test_13_00' ])
363+
364+
def test_13_01_string_select_encoding_utf8_behavior(self):
365+
self.adm(r"box.space['space_str']:insert{'test_13_01'}")
366+
367+
strdata = 'test_13_01'
368+
resp = self.con_encoding_utf8.select('space_str', [strdata])
369+
self.assertEquals(resp[0][0], strdata)
370+
371+
@skip_or_run_mp_bin_test
372+
@skip_or_run_varbinary_test
373+
def test_13_02_varbinary_insert_encoding_utf8_behavior(self):
374+
self.assertNotRaises(
375+
self.con_encoding_utf8.insert,
376+
'space_varbin', [ b'test_13_02' ])
377+
378+
@skip_or_run_mp_bin_test
379+
@skip_or_run_varbinary_test
380+
def test_13_03_varbinary_select_encoding_utf8_behavior(self):
381+
self.adm(r"""
382+
bintuple_insert(
383+
box.space['space_varbin'],
384+
{0xDE, 0xAD, 0xBE, 0xAF, 0x13, 0x03})
385+
""")
386+
387+
bindata = bytes(bytearray.fromhex('DEADBEAF1303'))
388+
resp = self.con_encoding_utf8.select('space_varbin', [bindata])
389+
self.assertEquals(resp[0][0], bindata)
390+
391+
def test_14_00_string_insert_encoding_none_behavior(self):
392+
self.assertNotRaises(
393+
self.con_encoding_none.insert,
394+
'space_str',
395+
[ bytes(bytearray.fromhex('DEADBEAF1400')) ])
396+
397+
def test_14_01_string_select_encoding_none_behavior(self):
398+
self.adm(r"box.space['space_str']:insert{'\xDE\xAD\xBE\xAF\x14\x01'}")
399+
400+
bindata = bytes(bytearray.fromhex('DEADBEAF1401'))
401+
resp = self.con_encoding_none.select('space_str', [bindata])
402+
self.assertEquals(resp[0][0], bindata)
403+
302404
@classmethod
303405
def tearDownClass(self):
304406
self.con.close()

0 commit comments

Comments
 (0)