Skip to content

Commit 797d33d

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 797d33d

File tree

4 files changed

+188
-21
lines changed

4 files changed

+188
-21
lines changed

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

tarantool/utils.py

+7-1
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:
@@ -20,7 +23,10 @@ def strxor(rhs, lhs):
2023
binary_types = (bytes, )
2124
string_types = (str, )
2225
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

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

test/suites/test_dml.py

+104
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,12 @@ 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)
2027
self.adm = self.srv.admin
2128
self.space_created = self.adm("box.schema.create_space('space_1')")
2229
self.adm("""
@@ -31,17 +38,64 @@ def setUpClass(self):
3138
parts = {2, 'num', 3, 'str'},
3239
unique = false})
3340
""".replace('\n', ' '))
41+
3442
self.space_created = self.adm("box.schema.create_space('space_2')")
3543
self.adm("""
3644
box.space['space_2']:create_index('primary', {
3745
type = 'hash',
3846
parts = {1, 'num'},
3947
unique = true})
4048
""".replace('\n', ' '))
49+
50+
self.adm("box.schema.create_space('space_str')")
51+
self.adm("""
52+
box.space['space_str']:create_index('primary', {
53+
type = 'tree',
54+
parts = {1, 'str'},
55+
unique = true})
56+
""".replace('\n', ' '))
57+
58+
self.adm("box.schema.create_space('space_varbin')")
59+
self.adm("""
60+
box.space['space_varbin']:create_index('primary', {
61+
type = 'tree',
62+
parts = {1, 'varbinary'},
63+
unique = true})
64+
""".replace('\n', ' '))
65+
self.adm("""
66+
buffer = require('buffer')
67+
ffi = require('ffi')
68+
69+
function encode_bin(bytes)
70+
local tmpbuf = buffer.ibuf()
71+
local p = tmpbuf:alloc(3 + #bytes)
72+
p[0] = 0x91
73+
p[1] = 0xC4
74+
p[2] = #bytes
75+
for i, c in pairs(bytes) do
76+
p[i + 3 - 1] = c
77+
end
78+
return tmpbuf
79+
end
80+
81+
function bintuple_insert(space, bytes)
82+
local tmpbuf = encode_bin(bytes)
83+
ffi.cdef[[
84+
int box_insert(uint32_t space_id, const char *tuple, const char *tuple_end, box_tuple_t **result);
85+
]]
86+
ffi.C.box_insert(space.id, tmpbuf.rpos, tmpbuf.wpos, nil)
87+
end
88+
""")
4189
self.adm("json = require('json')")
4290
self.adm("fiber = require('fiber')")
4391
self.adm("uuid = require('uuid')")
4492

93+
def assertNotRaises(self, func, *args, **kwargs):
94+
try:
95+
func(*args, **kwargs)
96+
except Exception as e:
97+
self.fail('Function raised Exception: %s' % repr(e))
98+
4599
def setUp(self):
46100
# prevent a remote tarantool from clean our session
47101
if self.srv.is_started():
@@ -55,6 +109,12 @@ def test_00_00_authenticate(self):
55109
box.schema.user.grant('test', 'execute,read,write', 'universe')
56110
"""))
57111
self.assertEqual(self.con.authenticate('test', 'test')._data, None)
112+
self.assertEqual(
113+
self.con_encoding_utf8.authenticate('test', 'test')._data, None
114+
)
115+
self.assertEqual(
116+
self.con_encoding_none.authenticate('test', 'test')._data, None
117+
)
58118

59119
def test_00_01_space_created(self):
60120
# Check that space is created in setUpClass
@@ -299,6 +359,50 @@ def test_12_update_fields(self):
299359
[[2, 'help', 7]]
300360
)
301361

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

0 commit comments

Comments
 (0)