Skip to content

Commit ee65860

Browse files
committed
Fix issues 130 and 131: Support Lua in Python 3 and improve mock signatures
1 parent ba41d90 commit ee65860

File tree

7 files changed

+105
-29
lines changed

7 files changed

+105
-29
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
*.py[co]
22

33
# Packages
4-
*.egg
4+
*.egg*
55
*.egg-info
66
dist
77
build

.travis.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
addons:
2+
apt:
3+
packages:
4+
- lua5.2
5+
- liblua5.2-dev
16
language: python
27
python:
38
- '2.7'
49
- '3.2'
510
- '3.3'
611
- '3.4'
12+
- '3.5'
13+
- '3.6'
714
- pypy
815
- pypy3
9-
install: pip install .
16+
install:
17+
- pip install .
18+
- if [[ "$TRAVIS_PYTHON_VERSION" =~ ^(2.7|3.4)$ ]]; then pip install .[lua]; fi
19+
# Add 3.2, 3.3, 3.5, and 3.6 when https://github.com/travis-ci/travis-ci/issues/8217 is fixed
1020
script: python setup.py nosetests
1121
deploy:
1222
provider: pypi

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ Use pip:
1414
## Usage
1515

1616
Both `mockredis.mock_redis_client` and `mockredis.mock_strict_redis_client` can be
17-
used to patch instances of the *redis client*.
17+
used to patch instances of the *redis client*, and `get_mock_redis_client_creator`
18+
can be used to create a generator for more flexible mocks.
1819

1920
For example, using the [mock][mock] library:
2021

@@ -24,6 +25,11 @@ Or:
2425

2526
@patch('redis.StrictRedis', mock_strict_redis_client)
2627

28+
Or, for more control:
29+
30+
@patch('redis.Redis', get_mock_redis_client_creator(load_lua_dependencies=False))
31+
@patch('redis.StrictRedis', get_mock_redis_client_creator(strict=True, clock=my_frozen_clock))
32+
2733
## Testing
2834

2935
Many unit tests exist to verify correctness of mock functionality. In addition, most

mockredis/client.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ def get_total_milliseconds(td):
15611561
return int((td.days * 24 * 60 * 60 + td.seconds) * 1000 + td.microseconds / 1000.0)
15621562

15631563

1564-
def mock_redis_client(**kwargs):
1564+
def mock_redis_client(*_, **__):
15651565
"""
15661566
Mock common.util.redis_client so we
15671567
can return a MockRedis object
@@ -1572,7 +1572,7 @@ def mock_redis_client(**kwargs):
15721572
mock_redis_client.from_url = mock_redis_client
15731573

15741574

1575-
def mock_strict_redis_client(**kwargs):
1575+
def mock_strict_redis_client(*_, **__):
15761576
"""
15771577
Mock common.util.redis_client so we
15781578
can return a MockRedis object
@@ -1581,3 +1581,22 @@ def mock_strict_redis_client(**kwargs):
15811581
return MockRedis(strict=True)
15821582

15831583
mock_strict_redis_client.from_url = mock_strict_redis_client
1584+
1585+
1586+
def get_mock_redis_client_creator(**kwargs):
1587+
"""
1588+
Generate a getter for a MockRedis
1589+
object that passes the given kwargs
1590+
to each MockRedis object instantiated
1591+
by the getter returned. Sample usage:
1592+
1593+
@mock.patch('redis.Redis', get_mock_redis_client_creator(load_lua_dependencies=False))
1594+
@mock.patch('redis.StrictRedis', get_mock_redis_client_creator(strict=True, clock=frozen_clock))
1595+
"""
1596+
1597+
def _getter(*_, **__):
1598+
return MockRedis(**kwargs)
1599+
1600+
_getter.from_url = _getter
1601+
1602+
return _getter

mockredis/script.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,26 @@
22
import threading
33
from mockredis.exceptions import ResponseError
44

5+
56
LuaLock = threading.Lock()
67

8+
if sys.version_info[0] == 3:
9+
_string_types = (str, )
10+
_integer_types = (int, )
11+
_number_types = (int, float)
12+
_string_or_binary_types = (str, bytes)
13+
_binary_type = bytes
14+
_long_type = int
15+
_iteritems = lambda d, **kw: iter(d.items(**kw))
16+
else:
17+
_string_types = (basestring, )
18+
_integer_types = (int, long)
19+
_number_types = (int, long, float)
20+
_string_or_binary_types = (basestring, )
21+
_binary_type = str
22+
_long_type = long
23+
_iteritems = lambda d, **kw: d.iteritems(**kw)
24+
725

826
class Script(object):
927
"""
@@ -47,7 +65,14 @@ def _call(*call_args):
4765
response = client.call(*call_args)
4866
return self._python_to_lua(response)
4967

50-
lua_globals.redis = {"call": _call}
68+
def _reply_table(field, message):
69+
return lua.eval("{{{field}='{message}'}}".format(field=field, message=message))
70+
71+
lua_globals.redis = {
72+
'call': _call,
73+
'status_reply': lambda status: _reply_table('ok', status),
74+
'error_reply': lambda error: _reply_table('err', error),
75+
}
5176
return self._lua_to_python(lua.execute(self.script), return_status=True)
5277

5378
@staticmethod
@@ -117,9 +142,12 @@ def _lua_to_python(lval, return_status=False):
117142
raise ResponseError(lval[i])
118143
pval.append(Script._lua_to_python(lval[i]))
119144
return pval
120-
elif isinstance(lval, long):
145+
elif lua_globals.type(lval) == "boolean":
146+
# Lua boolean --> Python bool
147+
return bool(lval)
148+
elif isinstance(lval, _integer_types):
121149
# Lua number --> Python long
122-
return long(lval)
150+
return _long_type(lval)
123151
elif isinstance(lval, float):
124152
# Lua number --> Python float
125153
return float(lval)
@@ -129,9 +157,6 @@ def _lua_to_python(lval, return_status=False):
129157
elif lua_globals.type(lval) == "string":
130158
# Lua string --> Python string
131159
return lval
132-
elif lua_globals.type(lval) == "boolean":
133-
# Lua boolean --> Python bool
134-
return bool(lval)
135160
raise RuntimeError("Invalid Lua type: " + str(lua_globals.type(lval)))
136161

137162
@staticmethod
@@ -161,17 +186,17 @@ def _python_to_lua(pval):
161186
# in Lua returns: {k1, v1, k2, v2, k3, v3}
162187
lua_dict = lua.eval("{}")
163188
lua_table = lua.eval("table")
164-
for k, v in pval.iteritems():
189+
for k, v in _iteritems(pval):
165190
lua_table.insert(lua_dict, Script._python_to_lua(k))
166191
lua_table.insert(lua_dict, Script._python_to_lua(v))
167192
return lua_dict
168-
elif isinstance(pval, str):
193+
elif isinstance(pval, _string_or_binary_types):
169194
# Python string --> Lua userdata
170195
return pval
171196
elif isinstance(pval, bool):
172197
# Python bool--> Lua boolean
173198
return lua.eval(str(pval).lower())
174-
elif isinstance(pval, (int, long, float)):
199+
elif isinstance(pval, _number_types):
175200
# Python int --> Lua number
176201
lua_globals = lua.globals()
177202
return lua_globals.tonumber(str(pval))

mockredis/tests/test_script.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
LIST1, LIST2,
1616
SET1,
1717
VAL1, VAL2, VAL3, VAL4,
18+
bVAL1, bVAL2, bVAL3, bVAL4,
1819
LPOP_SCRIPT
1920
)
20-
from mockredis.tests.fixtures import raises_response_error
2121

2222

2323
if sys.version_info >= (3, 0):
2424
long = int
25+
string_types = (str, )
26+
else:
27+
string_types = (basestring, )
2528

2629

2730
class TestScript(object):
@@ -108,7 +111,7 @@ def test_register_script_lpush(self):
108111
script(keys=[LIST1], args=[VAL1, VAL2])
109112

110113
# validate insertion
111-
eq_([VAL2, VAL1], self.redis.lrange(LIST1, 0, -1))
114+
eq_([bVAL2, bVAL1], self.redis.lrange(LIST1, 0, -1))
112115

113116
def test_register_script_lpop(self):
114117
self.redis.lpush(LIST1, VAL2, VAL1)
@@ -120,7 +123,7 @@ def test_register_script_lpop(self):
120123

121124
# validate lpop
122125
eq_(VAL1, list_item)
123-
eq_([VAL2], self.redis.lrange(LIST1, 0, -1))
126+
eq_([bVAL2], self.redis.lrange(LIST1, 0, -1))
124127

125128
def test_register_script_rpoplpush(self):
126129
self.redis.lpush(LIST1, VAL2, VAL1)
@@ -132,8 +135,8 @@ def test_register_script_rpoplpush(self):
132135
script(keys=[LIST1, LIST2])
133136

134137
# validate rpoplpush
135-
eq_([VAL1], self.redis.lrange(LIST1, 0, -1))
136-
eq_([VAL2, VAL3, VAL4], self.redis.lrange(LIST2, 0, -1))
138+
eq_([bVAL1], self.redis.lrange(LIST1, 0, -1))
139+
eq_([bVAL2, bVAL3, bVAL4], self.redis.lrange(LIST2, 0, -1))
137140

138141
def test_register_script_rpop_lpush(self):
139142
self.redis.lpush(LIST1, VAL2, VAL1)
@@ -148,8 +151,8 @@ def test_register_script_rpop_lpush(self):
148151
script(keys=[LIST1, LIST2])
149152

150153
# validate rpop and then lpush
151-
eq_([VAL1], self.redis.lrange(LIST1, 0, -1))
152-
eq_([VAL2, VAL3, VAL4], self.redis.lrange(LIST2, 0, -1))
154+
eq_([bVAL1], self.redis.lrange(LIST1, 0, -1))
155+
eq_([bVAL2, bVAL3, bVAL4], self.redis.lrange(LIST2, 0, -1))
153156

154157
def test_register_script_client(self):
155158
# lpush two values in LIST1 in first instance of redis
@@ -168,16 +171,16 @@ def test_register_script_client(self):
168171

169172
# validate lpop from LIST1 in redis2
170173
eq_(VAL3, list_item)
171-
eq_([VAL4], redis2.lrange(LIST1, 0, -1))
172-
eq_([VAL1, VAL2], self.redis.lrange(LIST1, 0, -1))
174+
eq_([bVAL4], redis2.lrange(LIST1, 0, -1))
175+
eq_([bVAL1, bVAL2], self.redis.lrange(LIST1, 0, -1))
173176

174177
def test_eval_lpush(self):
175178
# lpush two values
176179
script_content = "redis.call('LPUSH', KEYS[1], ARGV[1], ARGV[2])"
177180
self.redis.eval(script_content, 1, LIST1, VAL1, VAL2)
178181

179182
# validate insertion
180-
eq_([VAL2, VAL1], self.redis.lrange(LIST1, 0, -1))
183+
eq_([bVAL2, bVAL1], self.redis.lrange(LIST1, 0, -1))
181184

182185
def test_eval_lpop(self):
183186
self.redis.lpush(LIST1, VAL2, VAL1)
@@ -188,7 +191,7 @@ def test_eval_lpop(self):
188191

189192
# validate lpop
190193
eq_(VAL1, list_item)
191-
eq_([VAL2], self.redis.lrange(LIST1, 0, -1))
194+
eq_([bVAL2], self.redis.lrange(LIST1, 0, -1))
192195

193196
def test_eval_lrem(self):
194197
self.redis.delete(LIST1)
@@ -318,7 +321,7 @@ def test_lua_to_python_flota(self):
318321
def test_lua_to_python_string(self):
319322
lval = self.lua.eval('"somestring"')
320323
pval = MockRedisScript._lua_to_python(lval)
321-
ok_(isinstance(pval, str))
324+
ok_(isinstance(pval, string_types))
322325
eq_("somestring", pval)
323326

324327
def test_lua_to_python_bool(self):
@@ -383,11 +386,24 @@ def test_lua_ok_return(self):
383386
script = self.redis.register_script(script_content)
384387
eq_('OK', script())
385388

386-
@raises_response_error
387389
def test_lua_err_return(self):
388390
script_content = "return {err='ERROR Some message'}"
389391
script = self.redis.register_script(script_content)
390-
script()
392+
with assert_raises(Exception) as error_context:
393+
script()
394+
eq_('ERROR Some message', error_context.exception.args[0])
395+
396+
def test_lua_redis_status_reply(self):
397+
script_content = "return redis.status_reply('OK')"
398+
script = self.redis.register_script(script_content)
399+
eq_('OK', script())
400+
401+
def test_lua_redis_error_reply(self):
402+
script_content = "return redis.error_reply('my error')"
403+
script = self.redis.register_script(script_content)
404+
with assert_raises(Exception) as error_context:
405+
script()
406+
eq_('my error', error_context.exception.args[0])
391407

392408
def test_concurrent_lua(self):
393409
script_content = """

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
'nose'
1919
],
2020
extras_require={
21-
'lua': ['lunatic-python-bugfix==1.1.1'],
21+
'lua': ['lunatic-python-universal~=2.0'],
2222
},
2323
tests_require=[
2424
'redis>=2.9.0'

0 commit comments

Comments
 (0)