Skip to content

Commit a2d79da

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 str -> mp_str (string) mp_bin (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 69526d1 commit a2d79da

File tree

5 files changed

+274
-25
lines changed

5 files changed

+274
-25
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

+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

test/suites/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
from .test_mesh import TestSuite_Mesh
1313
from .test_execute import TestSuite_Execute
1414
from .test_dbapi import TestSuite_DBAPI
15+
from .test_encoding import TestSuite_Encoding
1516

1617
test_cases = (TestSuite_Schema_UnicodeConnection,
1718
TestSuite_Schema_BinaryConnection,
1819
TestSuite_Request, TestSuite_Protocol, TestSuite_Reconnect,
19-
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI)
20+
TestSuite_Mesh, TestSuite_Execute, TestSuite_DBAPI,
21+
TestSuite_Encoding)
2022

2123
def load_tests(loader, tests, pattern):
2224
suite = unittest.TestSuite()

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_encoding.py

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from __future__ import print_function
4+
5+
import sys
6+
import unittest
7+
import tarantool
8+
9+
from .lib.skip import skip_or_run_mp_bin_test, skip_or_run_varbinary_test
10+
from .lib.tarantool_server import TarantoolServer
11+
12+
class TestSuite_Encoding(unittest.TestCase):
13+
@classmethod
14+
def setUpClass(self):
15+
print(' ENCODING '.center(70, '='), file=sys.stderr)
16+
print('-' * 70, file=sys.stderr)
17+
self.srv = TarantoolServer()
18+
self.srv.script = 'test/suites/box.lua'
19+
self.srv.start()
20+
21+
self.srv.admin("""
22+
box.schema.user.create('test', { password = 'test' })
23+
box.schema.user.grant('test', 'execute,read,write', 'universe')
24+
""")
25+
26+
args = [self.srv.host, self.srv.args['primary']]
27+
kwargs = { 'user': 'test', 'password': 'test' }
28+
self.con_encoding_utf8 = tarantool.Connection(*args, encoding='utf-8', **kwargs)
29+
self.con_encoding_none = tarantool.Connection(*args, encoding=None, **kwargs)
30+
self.conns = [self.con_encoding_utf8, self.con_encoding_none]
31+
32+
self.srv.admin("box.schema.create_space('space_str')")
33+
self.srv.admin("""
34+
box.space['space_str']:create_index('primary', {
35+
type = 'tree',
36+
parts = {1, 'str'},
37+
unique = true})
38+
""".replace('\n', ' '))
39+
40+
self.srv.admin("box.schema.create_space('space_varbin')")
41+
self.srv.admin(r"""
42+
box.space['space_varbin']:format({
43+
{
44+
'id',
45+
type = 'number',
46+
is_nullable = false
47+
},
48+
{
49+
'varbin',
50+
type = 'varbinary',
51+
is_nullable = false,
52+
}
53+
})
54+
""".replace('\n', ' '))
55+
self.srv.admin("""
56+
box.space['space_varbin']:create_index('id', {
57+
type = 'tree',
58+
parts = {1, 'number'},
59+
unique = true})
60+
""".replace('\n', ' '))
61+
self.srv.admin("""
62+
box.space['space_varbin']:create_index('varbin', {
63+
type = 'tree',
64+
parts = {2, 'varbinary'},
65+
unique = true})
66+
""".replace('\n', ' '))
67+
68+
self.srv.admin(r"""
69+
local buffer = require('buffer')
70+
local ffi = require('ffi')
71+
72+
function encode_bin(bytes)
73+
local tmpbuf = buffer.ibuf()
74+
local p = tmpbuf:alloc(3 + #bytes)
75+
p[0] = 0x91
76+
p[1] = 0xC4
77+
p[2] = #bytes
78+
for i, c in pairs(bytes) do
79+
p[i + 3 - 1] = c
80+
end
81+
return tmpbuf
82+
end
83+
84+
function bintuple_insert(space, bytes)
85+
local tmpbuf = encode_bin(bytes)
86+
ffi.cdef[[
87+
int box_insert(uint32_t space_id, const char *tuple, const char *tuple_end, box_tuple_t **result);
88+
]]
89+
ffi.C.box_insert(space.id, tmpbuf.rpos, tmpbuf.wpos, nil)
90+
end
91+
""")
92+
93+
self.srv.admin("""
94+
function get_type(arg)
95+
return type(arg)
96+
end
97+
""")
98+
99+
def assertNotRaises(self, func, *args, **kwargs):
100+
try:
101+
func(*args, **kwargs)
102+
except Exception as e:
103+
self.fail('Function raised Exception: %s' % repr(e))
104+
105+
def setUp(self):
106+
# prevent a remote tarantool from clean our session
107+
if self.srv.is_started():
108+
self.srv.touch_lock()
109+
110+
# encoding = 'utf-8'
111+
#
112+
# Python 3 -> Tarantool -> Python 3
113+
# str -> mp_str (string) -> str
114+
# bytes -> mp_bin (varbinary) -> bytes
115+
def test_01_01_str_encode_for_encoding_utf8_behavior(self):
116+
self.assertNotRaises(
117+
self.con_encoding_utf8.insert,
118+
'space_str', [ 'test_01_01' ])
119+
120+
def test_01_02_string_decode_for_encoding_utf8_behavior(self):
121+
self.srv.admin(r"box.space['space_str']:insert{'test_01_02'}")
122+
123+
resp = self.con_encoding_utf8.eval("return box.space.space_str:get('test_01_02')")
124+
self.assertSequenceEqual(resp, [['test_01_02']])
125+
126+
@skip_or_run_mp_bin_test
127+
@skip_or_run_varbinary_test
128+
def test_01_03_bytes_encode_for_encoding_utf8_behavior(self):
129+
self.assertNotRaises(
130+
self.con_encoding_utf8.insert,
131+
'space_varbin', [ 103, bytes(bytearray.fromhex('DEADBEAF0103')) ])
132+
133+
@skip_or_run_mp_bin_test
134+
@skip_or_run_varbinary_test
135+
def test_01_04_varbinary_decode_for_encoding_utf8_behavior(self):
136+
self.con_encoding_utf8.execute(r"""
137+
INSERT INTO "space_varbin" VALUES (104, x'DEADBEAF0104');
138+
""")
139+
140+
resp = self.con_encoding_utf8.execute(r"""
141+
SELECT * FROM "space_varbin" WHERE "varbin" == x'DEADBEAF0104';
142+
""")
143+
self.assertSequenceEqual(resp, [[104, bytes(bytearray.fromhex('DEADBEAF0104'))]])
144+
145+
# encoding = None
146+
#
147+
# Python 3 -> Tarantool -> Python 3
148+
# bytes -> mp_str (string) -> bytes
149+
# str -> mp_str (string)
150+
# mp_bin (string) -> bytes
151+
def test_02_01_str_encode_for_encoding_none_behavior(self):
152+
self.assertNotRaises(
153+
self.con_encoding_none.insert,
154+
'space_str', [ 'test_02_01' ])
155+
156+
def test_02_02_string_decode_for_encoding_none_behavior(self):
157+
self.srv.admin(r"box.space['space_str']:insert{'test_02_02'}")
158+
159+
resp = self.con_encoding_none.eval("return box.space.space_str:get('test_02_02')")
160+
self.assertSequenceEqual(resp, [[b'test_02_02']])
161+
162+
def test_02_03_bytes_decode_for_encoding_utf8_behavior(self):
163+
self.assertNotRaises(
164+
self.con_encoding_none.insert,
165+
'space_str', [ b'test_02_03' ])
166+
167+
@skip_or_run_mp_bin_test
168+
@skip_or_run_varbinary_test
169+
def test_02_04_varbinary_decode_for_encoding_utf8_behavior(self):
170+
self.con_encoding_utf8.execute(r"""
171+
INSERT INTO "space_varbin" VALUES (204, x'DEADBEAF0204');
172+
""")
173+
174+
resp = self.con_encoding_utf8.execute(r"""
175+
SELECT * FROM "space_varbin" WHERE "varbin" == x'DEADBEAF0204';
176+
""")
177+
self.assertSequenceEqual(resp, [[204, bytes(bytearray.fromhex('DEADBEAF0204'))]])
178+
179+
@classmethod
180+
def tearDownClass(self):
181+
for con in self.conns:
182+
con.close()
183+
self.srv.stop()
184+
self.srv.clean()

0 commit comments

Comments
 (0)