Skip to content

Fix issues #130 and #131: Support Lua in Python 3 and improve mock signatures #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
*.py[co]

# Packages
*.egg
*.egg*
*.egg-info
dist
build
Expand Down
12 changes: 11 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
addons:
apt:
packages:
- lua5.2
- liblua5.2-dev
language: python
python:
- '2.7'
- '3.2'
- '3.3'
- '3.4'
- '3.5'
- '3.6'
- pypy
- pypy3
install: pip install .
install:
- pip install .
- if [[ "$TRAVIS_PYTHON_VERSION" =~ ^(2.7|3.4)$ ]]; then pip install .[lua]; fi
# Add 3.2, 3.3, 3.5, and 3.6 when https://github.com/travis-ci/travis-ci/issues/8217 is fixed
script: python setup.py nosetests
deploy:
provider: pypi
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ Use pip:
## Usage

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

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

Expand All @@ -24,6 +25,11 @@ Or:

@patch('redis.StrictRedis', mock_strict_redis_client)

Or, for more control:

@patch('redis.Redis', get_mock_redis_client_creator(load_lua_dependencies=False))
@patch('redis.StrictRedis', get_mock_redis_client_creator(strict=True, clock=my_frozen_clock))

## Testing

Many unit tests exist to verify correctness of mock functionality. In addition, most
Expand Down
23 changes: 21 additions & 2 deletions mockredis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1561,7 +1561,7 @@ def get_total_milliseconds(td):
return int((td.days * 24 * 60 * 60 + td.seconds) * 1000 + td.microseconds / 1000.0)


def mock_redis_client(**kwargs):
def mock_redis_client(*_, **__):
"""
Mock common.util.redis_client so we
can return a MockRedis object
Expand All @@ -1572,7 +1572,7 @@ def mock_redis_client(**kwargs):
mock_redis_client.from_url = mock_redis_client


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

mock_strict_redis_client.from_url = mock_strict_redis_client


def get_mock_redis_client_creator(**kwargs):
"""
Generate a getter for a MockRedis
object that passes the given kwargs
to each MockRedis object instantiated
by the getter returned. Sample usage:

@mock.patch('redis.Redis', get_mock_redis_client_creator(load_lua_dependencies=False))
@mock.patch('redis.StrictRedis', get_mock_redis_client_creator(strict=True, clock=frozen_clock))
"""

def _getter(*_, **__):
return MockRedis(**kwargs)

_getter.from_url = _getter

return _getter
43 changes: 34 additions & 9 deletions mockredis/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,26 @@
import threading
from mockredis.exceptions import ResponseError


LuaLock = threading.Lock()

if sys.version_info[0] == 3:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 2 should be the special case here. In future, when Python 4 gets released, this if will fallback to the logics for Python 2, which will definitely be incompatible with Python 4.

_string_types = (str, )
_integer_types = (int, )
_number_types = (int, float)
_string_or_binary_types = (str, bytes)
_binary_type = bytes
_long_type = int
_iteritems = lambda d, **kw: iter(d.items(**kw))
else:
_string_types = (basestring, )
_integer_types = (int, long)
_number_types = (int, long, float)
_string_or_binary_types = (basestring, )
_binary_type = str
_long_type = long
_iteritems = lambda d, **kw: d.iteritems(**kw)


class Script(object):
"""
Expand Down Expand Up @@ -47,7 +65,14 @@ def _call(*call_args):
response = client.call(*call_args)
return self._python_to_lua(response)

lua_globals.redis = {"call": _call}
def _reply_table(field, message):
return lua.eval("{{{field}='{message}'}}".format(field=field, message=message))

lua_globals.redis = {
'call': _call,
'status_reply': lambda status: _reply_table('ok', status),
'error_reply': lambda error: _reply_table('err', error),
}
return self._lua_to_python(lua.execute(self.script), return_status=True)

@staticmethod
Expand Down Expand Up @@ -117,9 +142,12 @@ def _lua_to_python(lval, return_status=False):
raise ResponseError(lval[i])
pval.append(Script._lua_to_python(lval[i]))
return pval
elif isinstance(lval, long):
elif lua_globals.type(lval) == "boolean":
# Lua boolean --> Python bool
return bool(lval)
elif isinstance(lval, _integer_types):
# Lua number --> Python long
return long(lval)
return _long_type(lval)
elif isinstance(lval, float):
# Lua number --> Python float
return float(lval)
Expand All @@ -129,9 +157,6 @@ def _lua_to_python(lval, return_status=False):
elif lua_globals.type(lval) == "string":
# Lua string --> Python string
return lval
elif lua_globals.type(lval) == "boolean":
# Lua boolean --> Python bool
return bool(lval)
raise RuntimeError("Invalid Lua type: " + str(lua_globals.type(lval)))

@staticmethod
Expand Down Expand Up @@ -161,17 +186,17 @@ def _python_to_lua(pval):
# in Lua returns: {k1, v1, k2, v2, k3, v3}
lua_dict = lua.eval("{}")
lua_table = lua.eval("table")
for k, v in pval.iteritems():
for k, v in _iteritems(pval):
lua_table.insert(lua_dict, Script._python_to_lua(k))
lua_table.insert(lua_dict, Script._python_to_lua(v))
return lua_dict
elif isinstance(pval, str):
elif isinstance(pval, _string_or_binary_types):
# Python string --> Lua userdata
return pval
elif isinstance(pval, bool):
# Python bool--> Lua boolean
return lua.eval(str(pval).lower())
elif isinstance(pval, (int, long, float)):
elif isinstance(pval, _number_types):
# Python int --> Lua number
lua_globals = lua.globals()
return lua_globals.tonumber(str(pval))
Expand Down
44 changes: 30 additions & 14 deletions mockredis/tests/test_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
LIST1, LIST2,
SET1,
VAL1, VAL2, VAL3, VAL4,
bVAL1, bVAL2, bVAL3, bVAL4,
LPOP_SCRIPT
)
from mockredis.tests.fixtures import raises_response_error


if sys.version_info >= (3, 0):
long = int
string_types = (str, )
else:
string_types = (basestring, )


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

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

def test_register_script_lpop(self):
self.redis.lpush(LIST1, VAL2, VAL1)
Expand All @@ -120,7 +123,7 @@ def test_register_script_lpop(self):

# validate lpop
eq_(VAL1, list_item)
eq_([VAL2], self.redis.lrange(LIST1, 0, -1))
eq_([bVAL2], self.redis.lrange(LIST1, 0, -1))

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

# validate rpoplpush
eq_([VAL1], self.redis.lrange(LIST1, 0, -1))
eq_([VAL2, VAL3, VAL4], self.redis.lrange(LIST2, 0, -1))
eq_([bVAL1], self.redis.lrange(LIST1, 0, -1))
eq_([bVAL2, bVAL3, bVAL4], self.redis.lrange(LIST2, 0, -1))

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

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

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

# validate lpop from LIST1 in redis2
eq_(VAL3, list_item)
eq_([VAL4], redis2.lrange(LIST1, 0, -1))
eq_([VAL1, VAL2], self.redis.lrange(LIST1, 0, -1))
eq_([bVAL4], redis2.lrange(LIST1, 0, -1))
eq_([bVAL1, bVAL2], self.redis.lrange(LIST1, 0, -1))

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

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

def test_eval_lpop(self):
self.redis.lpush(LIST1, VAL2, VAL1)
Expand All @@ -188,7 +191,7 @@ def test_eval_lpop(self):

# validate lpop
eq_(VAL1, list_item)
eq_([VAL2], self.redis.lrange(LIST1, 0, -1))
eq_([bVAL2], self.redis.lrange(LIST1, 0, -1))

def test_eval_lrem(self):
self.redis.delete(LIST1)
Expand Down Expand Up @@ -318,7 +321,7 @@ def test_lua_to_python_flota(self):
def test_lua_to_python_string(self):
lval = self.lua.eval('"somestring"')
pval = MockRedisScript._lua_to_python(lval)
ok_(isinstance(pval, str))
ok_(isinstance(pval, string_types))
eq_("somestring", pval)

def test_lua_to_python_bool(self):
Expand Down Expand Up @@ -383,11 +386,24 @@ def test_lua_ok_return(self):
script = self.redis.register_script(script_content)
eq_('OK', script())

@raises_response_error
def test_lua_err_return(self):
script_content = "return {err='ERROR Some message'}"
script = self.redis.register_script(script_content)
script()
with assert_raises(Exception) as error_context:
script()
eq_('ERROR Some message', error_context.exception.args[0])

def test_lua_redis_status_reply(self):
script_content = "return redis.status_reply('OK')"
script = self.redis.register_script(script_content)
eq_('OK', script())

def test_lua_redis_error_reply(self):
script_content = "return redis.error_reply('my error')"
script = self.redis.register_script(script_content)
with assert_raises(Exception) as error_context:
script()
eq_('my error', error_context.exception.args[0])

def test_concurrent_lua(self):
script_content = """
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
'nose'
],
extras_require={
'lua': ['lunatic-python-bugfix==1.1.1'],
'lua': ['lunatic-python-universal~=2.0'],
},
tests_require=[
'redis>=2.9.0'
Expand Down