diff --git a/.gitignore b/.gitignore index f24cd99..90fa2ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ *.py[co] # Packages -*.egg +*.egg* *.egg-info dist build diff --git a/.travis.yml b/.travis.yml index e21cb75..2207251 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index a4a5a41..303f14e 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 diff --git a/mockredis/client.py b/mockredis/client.py index 926e048..e447535 100644 --- a/mockredis/client.py +++ b/mockredis/client.py @@ -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 @@ -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 @@ -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 diff --git a/mockredis/script.py b/mockredis/script.py index c0a3d48..76327dc 100644 --- a/mockredis/script.py +++ b/mockredis/script.py @@ -2,8 +2,26 @@ import threading from mockredis.exceptions import ResponseError + LuaLock = threading.Lock() +if sys.version_info[0] == 3: + _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): """ @@ -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 @@ -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) @@ -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 @@ -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)) diff --git a/mockredis/tests/test_script.py b/mockredis/tests/test_script.py index bb000b6..d94a4fe 100644 --- a/mockredis/tests/test_script.py +++ b/mockredis/tests/test_script.py @@ -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): @@ -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) @@ -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) @@ -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) @@ -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 @@ -168,8 +171,8 @@ 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 @@ -177,7 +180,7 @@ def test_eval_lpush(self): 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) @@ -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) @@ -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): @@ -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 = """ diff --git a/setup.py b/setup.py index aa59785..aec7508 100755 --- a/setup.py +++ b/setup.py @@ -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'