From b8d997d551abf4eb628dca32dd201662ee106886 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 5 Feb 2018 12:54:20 -0500 Subject: [PATCH 01/50] Limited Lua support --- fakeredis.py | 39 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + test_fakeredis.py | 27 +++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/fakeredis.py b/fakeredis.py index 29e7196..17bc9d5 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -13,6 +13,8 @@ import types import re +from lupa import LuaRuntime + import redis from redis.exceptions import ResponseError import redis.client @@ -633,6 +635,43 @@ def sort(self, name, start=None, num=None, by=None, get=None, desc=False, except KeyError: return [] + def eval(self, script, numkeys, *keys_and_args): + """ + Execute the Lua ``script``, specifying the ``numkeys`` the script + will touch and the key names and argument values in ``keys_and_args``. + Returns the result of the script. + In practice, use the object returned by ``register_script``. This + function exists purely for Redis API completion. + """ + lua_runtime = LuaRuntime(unpack_returned_tuples=True) + + raw_lua = """ + function(KEYS, ARGV, callback) + redis = {{}} + redis.call = callback + {body} + end + """.format(body=script) + keys = (None,) + keys_and_args[:numkeys] + args = (None,) + keys_and_args[numkeys:] + + lua_func = lua_runtime.eval(raw_lua) + lua_func( + keys, + args, + self._lua_callback + ) + + def _lua_callback(self, op, *args): + special_cases = { + 'del': self.delete, + 'decrby': self.decr, + 'incrby': self.incr + } + op = op.lower() + func = special_cases[op] if op in special_cases else getattr(self, op) + return func(*args) + def _retrive_data_from_sort(self, data, get): if get is not None: if isinstance(get, string_types): diff --git a/requirements.txt b/requirements.txt index 9612303..ff4db96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ nose==1.3.4 redis==2.10.5 +lupa==1.6 \ No newline at end of file diff --git a/test_fakeredis.py b/test_fakeredis.py index c98a34b..cd24ab2 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2821,6 +2821,33 @@ def test_expire_should_not_handle_floating_point_values(self): self.redis.expire('some_unused_key', 1.2) self.redis.pexpire('some_unused_key', 1000.2) + def test_eval_delete(self): + self.redis.set('foo', 'bar') + val = self.redis.get('foo') + self.assertEqual(val, b'bar') + val = self.redis.eval('redis.call("DEL", KEYS[1])', 1, 'foo') + self.assertIsNone(val) + + def test_eval_set_value_to_arg(self): + self.redis.eval('redis.call("SET", KEYS[1], ARGV[1])', 1, 'foo', 'bar') + val = self.redis.get('foo') + self.assertEqual(val, b'bar') + + def test_eval_conditional(self): + lua = """ + local val = redis.call("GET", KEYS[1]) + if val == ARGV[1] then + redis.call("SET", KEYS[1], ARGV[2]) + else + redis.call("SET", KEYS[1], ARGV[1]) + end + """ + self.redis.eval(lua, 1, 'foo', 'bar', 'baz') + val = self.redis.get('foo') + self.assertEqual(val, b'bar') + self.redis.eval(lua, 1, 'foo', 'bar', 'baz') + val = self.redis.get('foo') + self.assertEqual(val, b'baz') class DecodeMixin(object): decode_responses = True From da8a82035a61c69e16c67f06bb00b73926bf17ba Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 5 Feb 2018 14:16:26 -0500 Subject: [PATCH 02/50] Bump patch version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35e0656..8d26c85 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='fakeredis', - version='0.9.0', + version='0.9.1', description="Fake implementation of redis API for testing purposes.", long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), From 084548494e981e4cecff0f08c5d51207277f42c0 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 5 Feb 2018 14:31:14 -0500 Subject: [PATCH 03/50] Remove nightly Python build --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 769d5ff..952275d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ python: - 3.4 - 3.5 - 3.6 - - nightly sudo: false cache: - pip From 7ea39cde838f60859cbb0678b5d48ba4b89b100f Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 5 Feb 2018 17:49:42 -0500 Subject: [PATCH 04/50] Return result from Lua --- fakeredis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fakeredis.py b/fakeredis.py index 17bc9d5..b584d4b 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -656,7 +656,7 @@ def eval(self, script, numkeys, *keys_and_args): args = (None,) + keys_and_args[numkeys:] lua_func = lua_runtime.eval(raw_lua) - lua_func( + return lua_func( keys, args, self._lua_callback From 74389b745f9ac6dc31aab8940b90203589b59faa Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 12:25:29 -0500 Subject: [PATCH 05/50] Fix table handling --- fakeredis.py | 14 ++++++++++++-- test_fakeredis.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index b584d4b..7bd44cd 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -12,8 +12,9 @@ import time import types import re +from itertools import count -from lupa import LuaRuntime +from lupa import LuaRuntime, lua_type import redis from redis.exceptions import ResponseError @@ -656,11 +657,20 @@ def eval(self, script, numkeys, *keys_and_args): args = (None,) + keys_and_args[numkeys:] lua_func = lua_runtime.eval(raw_lua) - return lua_func( + result = lua_func( keys, args, self._lua_callback ) + if lua_type(result) == 'table': + # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. + result_list = [] + for index in count(1): + if index not in result: + break + result_list.append(result[index]) + return result_list + return result def _lua_callback(self, op, *args): special_cases = { diff --git a/test_fakeredis.py b/test_fakeredis.py index cd24ab2..81a52da 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2849,6 +2849,23 @@ def test_eval_conditional(self): val = self.redis.get('foo') self.assertEqual(val, b'baz') + def test_eval_lrange(self): + self.redis.lpush("foo", "bar") + val = self.redis.eval('return redis.call("LRANGE", KEYS[1], 0, 1)', 1, 'foo') + self.assertEqual(val, [b'bar']) + + def test_eval_table(self): + lua = """ + local a = {} + a[1] = "foo" + a[2] = "bar" + a[17] = "baz" + return a + """ + val = self.redis.eval(lua, 0) + self.assertEqual(val, ["foo", "bar"]) + + class DecodeMixin(object): decode_responses = True From 58d076384a7a787207788030143d80d86b73ad2e Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 12:55:45 -0500 Subject: [PATCH 06/50] Fix unit tests --- test_fakeredis.py | 91 ++++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/test_fakeredis.py b/test_fakeredis.py index 81a52da..a9e1bbd 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2511,6 +2511,53 @@ def test_set_existing_key_persists(self): self.redis.set('foo', 'foo') self.assertEqual(self.redis.ttl('foo'), -1) + def test_eval_delete(self): + self.redis.set('foo', 'bar') + val = self.redis.get('foo') + self.assertEqual(val, b'bar') + val = self.redis.eval('redis.call("DEL", KEYS[1])', 1, 'foo') + self.assertIsNone(val) + + def test_eval_set_value_to_arg(self): + self.redis.eval('redis.call("SET", KEYS[1], ARGV[1])', 1, 'foo', 'bar') + val = self.redis.get('foo') + self.assertEqual(val, b'bar') + + def test_eval_conditional(self): + lua = """ + local val = redis.call("GET", KEYS[1]) + if val == ARGV[1] then + redis.call("SET", KEYS[1], ARGV[2]) + else + redis.call("SET", KEYS[1], ARGV[1]) + end + """ + self.redis.eval(lua, 1, 'foo', 'bar', 'baz') + val = self.redis.get('foo') + self.assertEqual(val, b'bar') + self.redis.eval(lua, 1, 'foo', 'bar', 'baz') + val = self.redis.get('foo') + self.assertEqual(val, b'baz') + + def test_eval_lrange(self): + self.redis.lpush("foo", "bar") + val = self.redis.eval('return redis.call("LRANGE", KEYS[1], 0, 1)', 1, 'foo') + self.assertEqual(val, [b'bar']) + + def test_eval_table(self): + self.redis.set("k1", "foo") + self.redis.set("k2", "bar") + self.redis.set("k3", "baz") + lua = """ + local a = {} + a[1] = redis.call('GET', 'k1') + a[2] = redis.call('GET', 'k2') + a[17] = redis.call('GET', 'k3') + return a + """ + val = self.redis.eval(lua, 0) + self.assertEqual(val, [b'foo', b'bar']) + class TestFakeRedis(unittest.TestCase): decode_responses = False @@ -2821,50 +2868,6 @@ def test_expire_should_not_handle_floating_point_values(self): self.redis.expire('some_unused_key', 1.2) self.redis.pexpire('some_unused_key', 1000.2) - def test_eval_delete(self): - self.redis.set('foo', 'bar') - val = self.redis.get('foo') - self.assertEqual(val, b'bar') - val = self.redis.eval('redis.call("DEL", KEYS[1])', 1, 'foo') - self.assertIsNone(val) - - def test_eval_set_value_to_arg(self): - self.redis.eval('redis.call("SET", KEYS[1], ARGV[1])', 1, 'foo', 'bar') - val = self.redis.get('foo') - self.assertEqual(val, b'bar') - - def test_eval_conditional(self): - lua = """ - local val = redis.call("GET", KEYS[1]) - if val == ARGV[1] then - redis.call("SET", KEYS[1], ARGV[2]) - else - redis.call("SET", KEYS[1], ARGV[1]) - end - """ - self.redis.eval(lua, 1, 'foo', 'bar', 'baz') - val = self.redis.get('foo') - self.assertEqual(val, b'bar') - self.redis.eval(lua, 1, 'foo', 'bar', 'baz') - val = self.redis.get('foo') - self.assertEqual(val, b'baz') - - def test_eval_lrange(self): - self.redis.lpush("foo", "bar") - val = self.redis.eval('return redis.call("LRANGE", KEYS[1], 0, 1)', 1, 'foo') - self.assertEqual(val, [b'bar']) - - def test_eval_table(self): - lua = """ - local a = {} - a[1] = "foo" - a[2] = "bar" - a[17] = "baz" - return a - """ - val = self.redis.eval(lua, 0) - self.assertEqual(val, ["foo", "bar"]) - class DecodeMixin(object): decode_responses = True From 77685dc311e4dbb38900b1b134e314f4c9ae85b3 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 13:01:10 -0500 Subject: [PATCH 07/50] Fix test --- test_fakeredis.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test_fakeredis.py b/test_fakeredis.py index a9e1bbd..d4cd84a 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2545,18 +2545,15 @@ def test_eval_lrange(self): self.assertEqual(val, [b'bar']) def test_eval_table(self): - self.redis.set("k1", "foo") - self.redis.set("k2", "bar") - self.redis.set("k3", "baz") lua = """ local a = {} - a[1] = redis.call('GET', 'k1') - a[2] = redis.call('GET', 'k2') - a[17] = redis.call('GET', 'k3') + a[1] = "foo" + a[2] = "bar" + a[17] = "baz" return a """ val = self.redis.eval(lua, 0) - self.assertEqual(val, [b'foo', b'bar']) + self.assertEqual(val, [u'foo', u'bar']) class TestFakeRedis(unittest.TestCase): From 0dd661f52f37cf2f1799f636fc0f00c7c9e4cb28 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 14:01:32 -0500 Subject: [PATCH 08/50] Decode non-byte strings --- fakeredis.py | 5 ++++- test_fakeredis.py | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index 7bd44cd..9265125 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -668,7 +668,10 @@ def eval(self, script, numkeys, *keys_and_args): for index in count(1): if index not in result: break - result_list.append(result[index]) + item = result[index] + result_list.append( + item.encode() if isinstance(item, str) and not isinstance(item, bytes) else item + ) return result_list return result diff --git a/test_fakeredis.py b/test_fakeredis.py index d4cd84a..0c97caf 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2553,7 +2553,16 @@ def test_eval_table(self): return a """ val = self.redis.eval(lua, 0) - self.assertEqual(val, [u'foo', u'bar']) + self.assertEqual(val, [b'foo', b'bar']) + + def test_eval_table_with_numbers(self): + lua = """ + local a = {} + a[1] = 42 + return a + """ + val = self.redis.eval(lua, 0) + self.assertEqual(val, [42]) class TestFakeRedis(unittest.TestCase): From 06e02b5d40e7b17ae69a4d850c3b64aab5b1144b Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 16:24:42 -0500 Subject: [PATCH 09/50] Restore nightly build --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 952275d..769d5ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - 3.4 - 3.5 - 3.6 + - nightly sudo: false cache: - pip From 0fd83e6472d20e593a3c4b5310c9b5ec46a8568e Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 16:32:02 -0500 Subject: [PATCH 10/50] Revert "Restore nightly build" This reverts commit 06e02b5d40e7b17ae69a4d850c3b64aab5b1144b. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 769d5ff..952275d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ python: - 3.4 - 3.5 - 3.6 - - nightly sudo: false cache: - pip From f2086fdcf0663c83ce300d127ca9779302e7b6d3 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 16:40:42 -0500 Subject: [PATCH 11/50] Update readme --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index 29db4d7..c30c85e 100644 --- a/README.rst +++ b/README.rst @@ -266,6 +266,11 @@ they have all been tagged as 'slow' so you can skip them by running:: Revision history ================ +0.9.5 +----- +Add support for StrictRedis.eval for Lua scripts +- `#9 `_ + 0.9.4 ----- This is a minor bugfix and optimization release: From 6e0c1b33a27599b5d42769b7749f52672a047a30 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Tue, 6 Feb 2018 16:43:05 -0500 Subject: [PATCH 12/50] Remove eval from unimplemented functions --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index c30c85e..f7dd566 100644 --- a/README.rst +++ b/README.rst @@ -203,7 +203,6 @@ scripting * script kill * script load * evalsha - * eval * script exists From 73cc1d7c4ab1fcbdf41eea5880756bfa4ce84c1c Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Wed, 7 Feb 2018 11:10:50 -0500 Subject: [PATCH 13/50] Code review fallout --- fakenewsredis.py | 35 ++++++++++++++++++++++++++++------- requirements.txt | 1 - setup.py | 5 ++++- test_fakenewsredis.py | 8 ++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index abca252..4d7a7eb 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -16,8 +16,6 @@ import functools from itertools import count -from lupa import LuaRuntime, lua_type - import redis from redis.exceptions import ResponseError import redis.client @@ -712,6 +710,8 @@ def eval(self, script, numkeys, *keys_and_args): In practice, use the object returned by ``register_script``. This function exists purely for Redis API completion. """ + from lupa import LuaRuntime, lua_type + lua_runtime = LuaRuntime(unpack_returned_tuples=True) raw_lua = """ @@ -744,14 +744,35 @@ def eval(self, script, numkeys, *keys_and_args): return result def _lua_callback(self, op, *args): + # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a + # ResponseError for consistency with Redis + commands = [ + 'APPEND', 'AUTH', 'BITCOUNT', 'BITFIELD', 'BITOP', 'BITPOS', 'BLPOP', 'BRPOP', 'BRPOPLPUSH', 'DECR', + 'DECRBY', 'DEL', 'DUMP', 'ECHO', 'EVAL', 'EVALSHA', 'EXISTS', 'EXPIRE', 'EXPIREAT', 'FLUSHALL', 'FLUSHDB', + 'GEOADD', 'GEODIST', 'GEOHASH', 'GEOPOS', 'GEORADIUS', 'GEORADIUSBYMEMBER', 'GET', 'GETBIT', 'GETRANGE', + 'GETSET', 'HDEL', 'HEXISTS', 'HGET', 'HGETALL', 'HINCRBY', 'HINCRBYFLOAT', 'HKEYS', 'HLEN', 'HMGET', + 'HMSET', 'HSCAN', 'HSET', 'HSETNX', 'HSTRLEN', 'HVALS', 'INCR', 'INCRBY', 'INCRBYFLOAT', 'INFO', 'KEYS', + 'LINDEX', 'LINSERT', 'LLEN', 'LPOP', 'LPUSH', 'LPUSHX', 'LRANGE', 'LREM', 'LSET', 'LTRIM', 'MGET', + 'MIGRATE', 'MOVE', 'MSET', 'MSETNX', 'OBJECT', 'PERSIST', 'PEXPIRE', 'PEXPIREAT', 'PFADD', 'PFCOUNT', + 'PFMERGE', 'PING', 'PSETEX', 'PSUBSCRIBE', 'PTTL', 'PUBLISH', 'PUBSUB', 'PUNSUBSCRIBE', 'RENAME', + 'RENAMENX', 'RESTORE', 'RPOP', 'RPOPLPUSH', 'RPUSH', 'RPUSHX', 'SADD', 'SCAN', 'SCARD', 'SDIFF', + 'SDIFFSTORE', 'SELECT', 'SET', 'SETBIT', 'SETEX', 'SETNX', 'SETRANGE', 'SHUTDOWN', 'SINTER', 'SINTERSTORE', + 'SISMEMBER', 'SLAVEOF', 'SLOWLOG', 'SMEMBERS', 'SMOVE', 'SORT', 'SPOP', 'SRANDMEMBER', 'SREM', 'SSCAN', + 'STRLEN', 'SUBSCRIBE', 'SUNION', 'SUNIONSTORE', 'SWAPDB', 'TOUCH', 'TTL', 'TYPE', 'UNLINK', 'UNSUBSCRIBE', + 'WAIT', 'WATCH', 'ZADD', 'ZCARD', 'ZCOUNT', 'ZINCRBY', 'ZINTERSTORE', 'ZLEXCOUNT', 'ZRANGE', 'ZRANGEBYLEX', + 'ZRANGEBYSCORE', 'ZRANK', 'ZREM', 'ZREMRANGEBYLEX', 'ZREMRANGEBYRANK', 'ZREMRANGEBYSCORE', 'ZREVRANGE', + 'ZREVRANGEBYLEX', 'ZREVRANGEBYSCORE', 'ZREVRANK', 'ZSCAN', 'ZSCORE', 'ZUNIONSTORE' + ] + if op.upper() not in commands: + raise ResponseError("Unknown Redis command called from Lua script") special_cases = { - 'del': self.delete, - 'decrby': self.decr, - 'incrby': self.incr + 'del': FakeStrictRedis.delete, + 'decrby': FakeStrictRedis.decr, + 'incrby': FakeStrictRedis.incr } op = op.lower() - func = special_cases[op] if op in special_cases else getattr(self, op) - return func(*args) + func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) + return func(self, *args) def _retrive_data_from_sort(self, data, get): if get is not None: diff --git a/requirements.txt b/requirements.txt index 4ac41b1..12cc5c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ flake8<3.0.0 nose==1.3.4 redis==2.10.6 -lupa==1.6 diff --git a/setup.py b/setup.py index dfe2376..2c74f98 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,8 @@ ], install_requires=[ 'redis', - ] + ], + extras_require=dict( + lua=['lupa==1.6'] + ) ) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index b4039a8..8aa2a2a 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3099,6 +3099,14 @@ def test_eval_table_with_numbers(self): val = self.redis.eval(lua, 0) self.assertEqual(val, [42]) + def test_eval_invalid_command(self): + with self.assertRaises(ResponseError): + self.redis.eval( + 'return redis.call("FOO")', + 0 + ) + + class TestFakeRedis(unittest.TestCase): decode_responses = False From 87c2686c51b0eec6ed4da73fe6afb8d6868f9984 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Wed, 7 Feb 2018 11:22:31 -0500 Subject: [PATCH 14/50] pep8: --- test_fakenewsredis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 8aa2a2a..5f82b09 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3107,7 +3107,6 @@ def test_eval_invalid_command(self): ) - class TestFakeRedis(unittest.TestCase): decode_responses = False From e0e77180e0739f0ae7c1a3248b9f58e49466249a Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Wed, 7 Feb 2018 11:32:10 -0500 Subject: [PATCH 15/50] Install lupa for CI --- .travis.yml | 1 + README.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index d484f0a..5f81d64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r requirements-26.txt; fi - pip install -r requirements.txt - pip install coverage python-coveralls + - pip install .[lua] before_script: - flake8 script: diff --git a/README.rst b/README.rst index f7dd566..941a64e 100644 --- a/README.rst +++ b/README.rst @@ -230,6 +230,7 @@ db=10 in order to minimize collisions with an existing redis instance. To run all the tests, install the requirements file:: pip install -r requirements.txt + pip install .[lua] If you just want to run the unittests:: From 121de8cfbc36b9b5f5b173b56b66a1dd19c54561 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Wed, 7 Feb 2018 11:10:50 -0500 Subject: [PATCH 16/50] Code review fallout --- fakeredis.py | 35 ++++++++++++++++++++++++++++------- requirements.txt | 1 - setup.py | 5 ++++- test_fakeredis.py | 8 ++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index 9265125..cd30a90 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -14,8 +14,6 @@ import re from itertools import count -from lupa import LuaRuntime, lua_type - import redis from redis.exceptions import ResponseError import redis.client @@ -644,6 +642,8 @@ def eval(self, script, numkeys, *keys_and_args): In practice, use the object returned by ``register_script``. This function exists purely for Redis API completion. """ + from lupa import LuaRuntime, lua_type + lua_runtime = LuaRuntime(unpack_returned_tuples=True) raw_lua = """ @@ -676,14 +676,35 @@ def eval(self, script, numkeys, *keys_and_args): return result def _lua_callback(self, op, *args): + # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a + # ResponseError for consistency with Redis + commands = [ + 'APPEND', 'AUTH', 'BITCOUNT', 'BITFIELD', 'BITOP', 'BITPOS', 'BLPOP', 'BRPOP', 'BRPOPLPUSH', 'DECR', + 'DECRBY', 'DEL', 'DUMP', 'ECHO', 'EVAL', 'EVALSHA', 'EXISTS', 'EXPIRE', 'EXPIREAT', 'FLUSHALL', 'FLUSHDB', + 'GEOADD', 'GEODIST', 'GEOHASH', 'GEOPOS', 'GEORADIUS', 'GEORADIUSBYMEMBER', 'GET', 'GETBIT', 'GETRANGE', + 'GETSET', 'HDEL', 'HEXISTS', 'HGET', 'HGETALL', 'HINCRBY', 'HINCRBYFLOAT', 'HKEYS', 'HLEN', 'HMGET', + 'HMSET', 'HSCAN', 'HSET', 'HSETNX', 'HSTRLEN', 'HVALS', 'INCR', 'INCRBY', 'INCRBYFLOAT', 'INFO', 'KEYS', + 'LINDEX', 'LINSERT', 'LLEN', 'LPOP', 'LPUSH', 'LPUSHX', 'LRANGE', 'LREM', 'LSET', 'LTRIM', 'MGET', + 'MIGRATE', 'MOVE', 'MSET', 'MSETNX', 'OBJECT', 'PERSIST', 'PEXPIRE', 'PEXPIREAT', 'PFADD', 'PFCOUNT', + 'PFMERGE', 'PING', 'PSETEX', 'PSUBSCRIBE', 'PTTL', 'PUBLISH', 'PUBSUB', 'PUNSUBSCRIBE', 'RENAME', + 'RENAMENX', 'RESTORE', 'RPOP', 'RPOPLPUSH', 'RPUSH', 'RPUSHX', 'SADD', 'SCAN', 'SCARD', 'SDIFF', + 'SDIFFSTORE', 'SELECT', 'SET', 'SETBIT', 'SETEX', 'SETNX', 'SETRANGE', 'SHUTDOWN', 'SINTER', 'SINTERSTORE', + 'SISMEMBER', 'SLAVEOF', 'SLOWLOG', 'SMEMBERS', 'SMOVE', 'SORT', 'SPOP', 'SRANDMEMBER', 'SREM', 'SSCAN', + 'STRLEN', 'SUBSCRIBE', 'SUNION', 'SUNIONSTORE', 'SWAPDB', 'TOUCH', 'TTL', 'TYPE', 'UNLINK', 'UNSUBSCRIBE', + 'WAIT', 'WATCH', 'ZADD', 'ZCARD', 'ZCOUNT', 'ZINCRBY', 'ZINTERSTORE', 'ZLEXCOUNT', 'ZRANGE', 'ZRANGEBYLEX', + 'ZRANGEBYSCORE', 'ZRANK', 'ZREM', 'ZREMRANGEBYLEX', 'ZREMRANGEBYRANK', 'ZREMRANGEBYSCORE', 'ZREVRANGE', + 'ZREVRANGEBYLEX', 'ZREVRANGEBYSCORE', 'ZREVRANK', 'ZSCAN', 'ZSCORE', 'ZUNIONSTORE' + ] + if op.upper() not in commands: + raise ResponseError("Unknown Redis command called from Lua script") special_cases = { - 'del': self.delete, - 'decrby': self.decr, - 'incrby': self.incr + 'del': FakeStrictRedis.delete, + 'decrby': FakeStrictRedis.decr, + 'incrby': FakeStrictRedis.incr } op = op.lower() - func = special_cases[op] if op in special_cases else getattr(self, op) - return func(*args) + func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) + return func(self, *args) def _retrive_data_from_sort(self, data, get): if get is not None: diff --git a/requirements.txt b/requirements.txt index ff4db96..9612303 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ nose==1.3.4 redis==2.10.5 -lupa==1.6 \ No newline at end of file diff --git a/setup.py b/setup.py index 8d26c85..0465c46 100644 --- a/setup.py +++ b/setup.py @@ -28,5 +28,8 @@ ], install_requires=[ 'redis', - ] + ], + extras_require=dict( + lua=['lupa==1.6'] + ) ) diff --git a/test_fakeredis.py b/test_fakeredis.py index 0c97caf..1036bfb 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2564,6 +2564,14 @@ def test_eval_table_with_numbers(self): val = self.redis.eval(lua, 0) self.assertEqual(val, [42]) + def test_eval_invalid_command(self): + with self.assertRaises(ResponseError): + self.redis.eval( + 'return redis.call("FOO")', + 0 + ) + + class TestFakeRedis(unittest.TestCase): decode_responses = False From 9de875482e01f75b72895a5ee35999c2745cf8ae Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Wed, 7 Feb 2018 11:22:31 -0500 Subject: [PATCH 17/50] pep8: --- test_fakeredis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_fakeredis.py b/test_fakeredis.py index 1036bfb..ece4f71 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2572,7 +2572,6 @@ def test_eval_invalid_command(self): ) - class TestFakeRedis(unittest.TestCase): decode_responses = False From 9e6b32f29fbe6f6a16ec2a327c14b53c38e96a2f Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Wed, 7 Feb 2018 11:32:10 -0500 Subject: [PATCH 18/50] Install lupa for CI --- .travis.yml | 1 + README.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 952275d..892485f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r requirements-26.txt; fi - pip install -r requirements.txt - pip install coverage python-coveralls + - pip install .[lua] script: - coverage erase - coverage run --source fakeredis.py test_fakeredis.py diff --git a/README.rst b/README.rst index 3adb65c..c7041b2 100644 --- a/README.rst +++ b/README.rst @@ -227,6 +227,7 @@ db=10 in order to minimize collisions with an existing redis instance. To run all the tests, install the requirements file:: pip install -r requirements.txt + pip install .[lua] If you just want to run the unittests:: From 9bf417340749ccee830428c11d667c7de5e118cf Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 11:52:18 -0500 Subject: [PATCH 19/50] More fallout --- README.rst | 1 - fakenewsredis.py | 87 ++++++++++++++++++++++++++++++++----------- requirements.txt | 1 + setup.py | 6 +-- test_fakenewsredis.py | 50 +++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index 941a64e..f7dd566 100644 --- a/README.rst +++ b/README.rst @@ -230,7 +230,6 @@ db=10 in order to minimize collisions with an existing redis instance. To run all the tests, install the requirements file:: pip install -r requirements.txt - pip install .[lua] If you just want to run the unittests:: diff --git a/fakenewsredis.py b/fakenewsredis.py index 4d7a7eb..793a6ad 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -710,40 +710,82 @@ def eval(self, script, numkeys, *keys_and_args): In practice, use the object returned by ``register_script``. This function exists purely for Redis API completion. """ - from lupa import LuaRuntime, lua_type + from lupa import LuaRuntime, lua_type, LuaSyntaxError lua_runtime = LuaRuntime(unpack_returned_tuples=True) - raw_lua = """ - function(KEYS, ARGV, callback) - redis = {{}} - redis.call = callback - {body} - end - """.format(body=script) - keys = (None,) + keys_and_args[:numkeys] - args = (None,) + keys_and_args[numkeys:] - - lua_func = lua_runtime.eval(raw_lua) - result = lua_func( - keys, - args, - self._lua_callback + set_globals = lua_runtime.eval( + """ + function(keys, argv, callback) + redis = {{}} + redis.call = callback + redis.error_reply = function(msg) return {err=msg} end + redis.status_reply = function(msg) return {ok=msg} end + KEYS = keys + ARGV = argv + end + """ ) + expected_globals = set() + set_globals( + (None,) + keys_and_args[:numkeys], + (None,) + keys_and_args[numkeys:], + functools.partial(self._lua_callback, lua_runtime, expected_globals) + ) + expected_globals.update(lua_runtime.globals().keys()) + + try: + result = lua_runtime.execute(script) + except LuaSyntaxError as ex: + raise ResponseError(ex) + + self._check_for_lua_globals(lua_runtime, expected_globals) + + return self._decode_lua_result(result, nested=False) + + def _decode_lua_result(self, result, nested=True): + from lupa import lua_type + if lua_type(result) == 'table': + if not nested: + for key in ('ok', 'err'): + if key in result: + msg = result[key] + if not isinstance(msg, str): + raise ResponseError("wrong number or type of arguments") + decoded = self._decode_lua_result(msg) + if key == 'ok': + return decoded + else: + raise ResponseError(decoded) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in count(1): if index not in result: break item = result[index] - result_list.append( - item.encode() if isinstance(item, str) and not isinstance(item, bytes) else item - ) + result_list.append(self._decode_lua_result(item)) return result_list + elif isinstance(result, str) and not isinstance(result, bytes): + return result.encode() + elif isinstance(result, float): + return int(result) + elif isinstance(result, bool): + return result and 1 or None return result - def _lua_callback(self, op, *args): + def _check_for_lua_globals(self, lua_runtime, expected_globals): + actual_globals = set(lua_runtime.globals().keys()) + if actual_globals != expected_globals: + raise ResponseError( + "Script attempted to set a global variables: %s" % ", ".join( + actual_globals - expected_globals + ) + ) + + def _lua_callback(self, lua_runtime, expected_globals, op, *args): + # Check if we've set any global variables before making any change. + self._check_for_lua_globals(lua_runtime, expected_globals) # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a # ResponseError for consistency with Redis commands = [ @@ -772,7 +814,10 @@ def _lua_callback(self, op, *args): } op = op.lower() func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) - return func(self, *args) + try: + return func(self, *args) + except Exception as ex: + raise ResponseError(ex) def _retrive_data_from_sort(self, data, get): if get is not None: diff --git a/requirements.txt b/requirements.txt index 12cc5c9..00fab91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ flake8<3.0.0 nose==1.3.4 redis==2.10.6 +lupa==1.6 \ No newline at end of file diff --git a/setup.py b/setup.py index 2c74f98..56224f2 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ install_requires=[ 'redis', ], - extras_require=dict( - lua=['lupa==1.6'] - ) + extras_require={ + "lua": ['lupa'] + } ) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 5f82b09..2ea7821 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3099,6 +3099,16 @@ def test_eval_table_with_numbers(self): val = self.redis.eval(lua, 0) self.assertEqual(val, [42]) + def test_eval_nested_table(self): + lua = """ + local a = {} + a[1] = {} + a[1][1] = "foo" + return a + """ + val = self.redis.eval(lua, 0) + self.assertEqual(val, [[b'foo']]) + def test_eval_invalid_command(self): with self.assertRaises(ResponseError): self.redis.eval( @@ -3106,6 +3116,46 @@ def test_eval_invalid_command(self): 0 ) + def test_eval_syntax_error(self): + with self.assertRaises(ResponseError): + self.redis.eval('return "', 0) + + def test_eval_global_variable(self): + # Redis doesn't allow script to define global variables + with self.assertRaises(ResponseError): + self.redis.eval('a=10', 0) + + def test_eval_convert_number(self): + # Redis forces all Lua numbers to integer + val = self.redis.eval('return 3.2', 0) + self.assertEqual(val, 3) + val = self.redis.eval('return 3.8', 0) + self.assertEqual(val, 3) + val = self.redis.eval('return -3.8', 0) + self.assertEqual(val, -3) + + def test_eval_convert_bool(self): + # Redis converts true to 1 and false to nil (which redis-py converts to None) + val = self.redis.eval('return false', 0) + self.assertIsNone(val) + val = self.redis.eval('return true', 0) + self.assertEqual(val, 1) + self.assertNotIsInstance(val, bool) + + def test_eval_return_error(self): + with self.assertRaises(redis.ResponseError) as cm: + self.redis.eval('return {err="Testing"}', 0) + self.assertIn('Testing', str(cm.exception)) + with self.assertRaises(redis.ResponseError) as cm: + self.redis.eval('return redis.error_reply("Testing")', 0) + self.assertIn('Testing', str(cm.exception)) + + def test_eval_return_ok(self): + val = self.redis.eval('return {ok="Testing"}', 0) + self.assertEqual(val, b'Testing') + val = self.redis.eval('return redis.status_reply("Testing")', 0) + self.assertEqual(val, b'Testing') + class TestFakeRedis(unittest.TestCase): decode_responses = False From bfa38c9aee4b02433cc75b4b12681cd60ab0a4e5 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 11:52:57 -0500 Subject: [PATCH 20/50] Remove lua from .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5f81d64..d484f0a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r requirements-26.txt; fi - pip install -r requirements.txt - pip install coverage python-coveralls - - pip install .[lua] before_script: - flake8 script: From 63949bad5ba3799146fc6e5d13688261cfdb9c39 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:00:34 -0500 Subject: [PATCH 21/50] More tests --- test_fakenewsredis.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 2ea7821..4cae3b7 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3125,6 +3125,17 @@ def test_eval_global_variable(self): with self.assertRaises(ResponseError): self.redis.eval('a=10', 0) + def test_eval_global_and_return_ok(self): + # Redis doesn't allow script to define global variables + with self.assertRaises(ResponseError): + self.redis.eval( + ''' + a=10 + return redis.status_reply("Everything is awesome") + ''', + 0 + ) + def test_eval_convert_number(self): # Redis forces all Lua numbers to integer val = self.redis.eval('return 3.2', 0) @@ -3156,6 +3167,10 @@ def test_eval_return_ok(self): val = self.redis.eval('return redis.status_reply("Testing")', 0) self.assertEqual(val, b'Testing') + def test_eval_return_ok_wrong_type(self): + with self.assertRaises(redis.ResponseError): + self.redis.eval('return redis.status_reply(123)', 0) + class TestFakeRedis(unittest.TestCase): decode_responses = False From 975f88c0eaf4b48e9ab92eadfafee40d2a550873 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:21:43 -0500 Subject: [PATCH 22/50] pcall --- fakenewsredis.py | 41 ++++++++++++++++++++++++++--------------- test_fakenewsredis.py | 16 ++++++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index 793a6ad..a4746b2 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -716,9 +716,10 @@ def eval(self, script, numkeys, *keys_and_args): set_globals = lua_runtime.eval( """ - function(keys, argv, callback) + function(keys, argv, redis_call, redis_pcall) redis = {{}} - redis.call = callback + redis.call = redis_call + redis.pcall = redis_pcall redis.error_reply = function(msg) return {err=msg} end redis.status_reply = function(msg) return {ok=msg} end KEYS = keys @@ -730,7 +731,8 @@ def eval(self, script, numkeys, *keys_and_args): set_globals( (None,) + keys_and_args[:numkeys], (None,) + keys_and_args[numkeys:], - functools.partial(self._lua_callback, lua_runtime, expected_globals) + functools.partial(self._lua_redis_call, lua_runtime, expected_globals), + functools.partial(self._lua_redis_pcall, lua_runtime, expected_globals) ) expected_globals.update(lua_runtime.globals().keys()) @@ -747,17 +749,18 @@ def _decode_lua_result(self, result, nested=True): from lupa import lua_type if lua_type(result) == 'table': - if not nested: - for key in ('ok', 'err'): - if key in result: - msg = result[key] - if not isinstance(msg, str): - raise ResponseError("wrong number or type of arguments") - decoded = self._decode_lua_result(msg) - if key == 'ok': - return decoded - else: - raise ResponseError(decoded) + for key in ('ok', 'err'): + if key in result: + msg = result[key] + if not isinstance(msg, str): + raise ResponseError("wrong number or type of arguments") + decoded = self._decode_lua_result(msg) + if key == 'ok': + return decoded + elif nested: + return ResponseError(decoded) + else: + raise ResponseError(decoded) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in count(1): @@ -783,7 +786,15 @@ def _check_for_lua_globals(self, lua_runtime, expected_globals): ) ) - def _lua_callback(self, lua_runtime, expected_globals, op, *args): + def _lua_redis_pcall(self, lua_runtime, expected_globals, op, *args): + try: + return self._lua_redis_call(lua_runtime, expected_globals, op, *args) + except Exception as ex: + return lua_runtime.table_from( + {"err": str(ex)} + ) + + def _lua_redis_call(self, lua_runtime, expected_globals, op, *args): # Check if we've set any global variables before making any change. self._check_for_lua_globals(lua_runtime, expected_globals) # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 4cae3b7..fe9b746 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3171,6 +3171,22 @@ def test_eval_return_ok_wrong_type(self): with self.assertRaises(redis.ResponseError): self.redis.eval('return redis.status_reply(123)', 0) + def test_pcall(self): + val = self.redis.eval( + ''' + local a = {} + a[1] = redis.pcall("foo") + return a + ''', + 0 + ) + self.assertIsInstance(val, list) + self.assertEqual(len(val), 1) + self.assertIsInstance(val[0], ResponseError) + + def test_pcall_return_value(self): + with self.assertRaises(ResponseError): + val = self.redis.eval('return redis.pcall("foo")', 0) class TestFakeRedis(unittest.TestCase): decode_responses = False From 71a2c1e14f9960e15070a8fd05a708a7a8ddcb8a Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:24:01 -0500 Subject: [PATCH 23/50] Test nested ok response --- test_fakenewsredis.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index fe9b746..65d0fbc 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3167,6 +3167,17 @@ def test_eval_return_ok(self): val = self.redis.eval('return redis.status_reply("Testing")', 0) self.assertEqual(val, b'Testing') + def test_eval_return_ok_nested(self): + val = self.redis.eval( + ''' + local a = {} + a[1] = {ok="Testing"} + return a + ''', + 0 + ) + self.assertEqual(val, [b'Testing']) + def test_eval_return_ok_wrong_type(self): with self.assertRaises(redis.ResponseError): self.redis.eval('return redis.status_reply(123)', 0) From 1def833c7e9ce9b591a4ad841695e748f76f7db9 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:29:12 -0500 Subject: [PATCH 24/50] pep8 --- fakenewsredis.py | 2 +- test_fakenewsredis.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index a4746b2..b953932 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -710,7 +710,7 @@ def eval(self, script, numkeys, *keys_and_args): In practice, use the object returned by ``register_script``. This function exists purely for Redis API completion. """ - from lupa import LuaRuntime, lua_type, LuaSyntaxError + from lupa import LuaRuntime, LuaSyntaxError lua_runtime = LuaRuntime(unpack_returned_tuples=True) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 65d0fbc..74712a5 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3197,7 +3197,8 @@ def test_pcall(self): def test_pcall_return_value(self): with self.assertRaises(ResponseError): - val = self.redis.eval('return redis.pcall("foo")', 0) + self.redis.eval('return redis.pcall("foo")', 0) + class TestFakeRedis(unittest.TestCase): decode_responses = False From dd23fb10559f5443eb9eff0da051b7774782cffb Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:52:07 -0500 Subject: [PATCH 25/50] Fix broken tst --- fakenewsredis.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index b953932..f5ca68d 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -751,16 +751,15 @@ def _decode_lua_result(self, result, nested=True): if lua_type(result) == 'table': for key in ('ok', 'err'): if key in result: - msg = result[key] + msg = self._decode_lua_result(result[key]) if not isinstance(msg, str): - raise ResponseError("wrong number or type of arguments") - decoded = self._decode_lua_result(msg) + raise ResponseError("wrong number or type of arguments: %s" % repr(msg)) if key == 'ok': - return decoded + return msg elif nested: - return ResponseError(decoded) + return ResponseError(msg) else: - raise ResponseError(decoded) + raise ResponseError(msg) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in count(1): @@ -769,7 +768,7 @@ def _decode_lua_result(self, result, nested=True): item = result[index] result_list.append(self._decode_lua_result(item)) return result_list - elif isinstance(result, str) and not isinstance(result, bytes): + elif isinstance(result, type(u'')): return result.encode() elif isinstance(result, float): return int(result) From c91f0b142248252110b0dd686739bbf89bd7ccb3 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 11:52:18 -0500 Subject: [PATCH 26/50] More fallout --- README.rst | 1 - fakeredis.py | 87 +++++++++++++++++++++++++++++++++++------------ requirements.txt | 1 + setup.py | 6 ++-- test_fakeredis.py | 50 +++++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index c7041b2..3adb65c 100644 --- a/README.rst +++ b/README.rst @@ -227,7 +227,6 @@ db=10 in order to minimize collisions with an existing redis instance. To run all the tests, install the requirements file:: pip install -r requirements.txt - pip install .[lua] If you just want to run the unittests:: diff --git a/fakeredis.py b/fakeredis.py index cd30a90..73460cb 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -642,40 +642,82 @@ def eval(self, script, numkeys, *keys_and_args): In practice, use the object returned by ``register_script``. This function exists purely for Redis API completion. """ - from lupa import LuaRuntime, lua_type + from lupa import LuaRuntime, lua_type, LuaSyntaxError lua_runtime = LuaRuntime(unpack_returned_tuples=True) - raw_lua = """ - function(KEYS, ARGV, callback) - redis = {{}} - redis.call = callback - {body} - end - """.format(body=script) - keys = (None,) + keys_and_args[:numkeys] - args = (None,) + keys_and_args[numkeys:] - - lua_func = lua_runtime.eval(raw_lua) - result = lua_func( - keys, - args, - self._lua_callback + set_globals = lua_runtime.eval( + """ + function(keys, argv, callback) + redis = {{}} + redis.call = callback + redis.error_reply = function(msg) return {err=msg} end + redis.status_reply = function(msg) return {ok=msg} end + KEYS = keys + ARGV = argv + end + """ ) + expected_globals = set() + set_globals( + (None,) + keys_and_args[:numkeys], + (None,) + keys_and_args[numkeys:], + functools.partial(self._lua_callback, lua_runtime, expected_globals) + ) + expected_globals.update(lua_runtime.globals().keys()) + + try: + result = lua_runtime.execute(script) + except LuaSyntaxError as ex: + raise ResponseError(ex) + + self._check_for_lua_globals(lua_runtime, expected_globals) + + return self._decode_lua_result(result, nested=False) + + def _decode_lua_result(self, result, nested=True): + from lupa import lua_type + if lua_type(result) == 'table': + if not nested: + for key in ('ok', 'err'): + if key in result: + msg = result[key] + if not isinstance(msg, str): + raise ResponseError("wrong number or type of arguments") + decoded = self._decode_lua_result(msg) + if key == 'ok': + return decoded + else: + raise ResponseError(decoded) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in count(1): if index not in result: break item = result[index] - result_list.append( - item.encode() if isinstance(item, str) and not isinstance(item, bytes) else item - ) + result_list.append(self._decode_lua_result(item)) return result_list + elif isinstance(result, str) and not isinstance(result, bytes): + return result.encode() + elif isinstance(result, float): + return int(result) + elif isinstance(result, bool): + return result and 1 or None return result - def _lua_callback(self, op, *args): + def _check_for_lua_globals(self, lua_runtime, expected_globals): + actual_globals = set(lua_runtime.globals().keys()) + if actual_globals != expected_globals: + raise ResponseError( + "Script attempted to set a global variables: %s" % ", ".join( + actual_globals - expected_globals + ) + ) + + def _lua_callback(self, lua_runtime, expected_globals, op, *args): + # Check if we've set any global variables before making any change. + self._check_for_lua_globals(lua_runtime, expected_globals) # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a # ResponseError for consistency with Redis commands = [ @@ -704,7 +746,10 @@ def _lua_callback(self, op, *args): } op = op.lower() func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) - return func(self, *args) + try: + return func(self, *args) + except Exception as ex: + raise ResponseError(ex) def _retrive_data_from_sort(self, data, get): if get is not None: diff --git a/requirements.txt b/requirements.txt index 9612303..5351e4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ nose==1.3.4 redis==2.10.5 +lupa==1.6 diff --git a/setup.py b/setup.py index 0465c46..4d03f67 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,7 @@ install_requires=[ 'redis', ], - extras_require=dict( - lua=['lupa==1.6'] - ) + extras_require={ + "lua": ['lupa'] + } ) diff --git a/test_fakeredis.py b/test_fakeredis.py index ece4f71..60cb303 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2564,6 +2564,16 @@ def test_eval_table_with_numbers(self): val = self.redis.eval(lua, 0) self.assertEqual(val, [42]) + def test_eval_nested_table(self): + lua = """ + local a = {} + a[1] = {} + a[1][1] = "foo" + return a + """ + val = self.redis.eval(lua, 0) + self.assertEqual(val, [[b'foo']]) + def test_eval_invalid_command(self): with self.assertRaises(ResponseError): self.redis.eval( @@ -2571,6 +2581,46 @@ def test_eval_invalid_command(self): 0 ) + def test_eval_syntax_error(self): + with self.assertRaises(ResponseError): + self.redis.eval('return "', 0) + + def test_eval_global_variable(self): + # Redis doesn't allow script to define global variables + with self.assertRaises(ResponseError): + self.redis.eval('a=10', 0) + + def test_eval_convert_number(self): + # Redis forces all Lua numbers to integer + val = self.redis.eval('return 3.2', 0) + self.assertEqual(val, 3) + val = self.redis.eval('return 3.8', 0) + self.assertEqual(val, 3) + val = self.redis.eval('return -3.8', 0) + self.assertEqual(val, -3) + + def test_eval_convert_bool(self): + # Redis converts true to 1 and false to nil (which redis-py converts to None) + val = self.redis.eval('return false', 0) + self.assertIsNone(val) + val = self.redis.eval('return true', 0) + self.assertEqual(val, 1) + self.assertNotIsInstance(val, bool) + + def test_eval_return_error(self): + with self.assertRaises(redis.ResponseError) as cm: + self.redis.eval('return {err="Testing"}', 0) + self.assertIn('Testing', str(cm.exception)) + with self.assertRaises(redis.ResponseError) as cm: + self.redis.eval('return redis.error_reply("Testing")', 0) + self.assertIn('Testing', str(cm.exception)) + + def test_eval_return_ok(self): + val = self.redis.eval('return {ok="Testing"}', 0) + self.assertEqual(val, b'Testing') + val = self.redis.eval('return redis.status_reply("Testing")', 0) + self.assertEqual(val, b'Testing') + class TestFakeRedis(unittest.TestCase): decode_responses = False From ee53358ea96acc4ac0ce9d2a8b98c68d63a66b1e Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 11:52:57 -0500 Subject: [PATCH 27/50] Remove lua from .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 892485f..952275d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,6 @@ install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install -r requirements-26.txt; fi - pip install -r requirements.txt - pip install coverage python-coveralls - - pip install .[lua] script: - coverage erase - coverage run --source fakeredis.py test_fakeredis.py From a30fdeffa3642f1df45370b7dcd617e1cf321050 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:00:34 -0500 Subject: [PATCH 28/50] More tests --- test_fakeredis.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test_fakeredis.py b/test_fakeredis.py index 60cb303..2c54886 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2590,6 +2590,17 @@ def test_eval_global_variable(self): with self.assertRaises(ResponseError): self.redis.eval('a=10', 0) + def test_eval_global_and_return_ok(self): + # Redis doesn't allow script to define global variables + with self.assertRaises(ResponseError): + self.redis.eval( + ''' + a=10 + return redis.status_reply("Everything is awesome") + ''', + 0 + ) + def test_eval_convert_number(self): # Redis forces all Lua numbers to integer val = self.redis.eval('return 3.2', 0) @@ -2621,6 +2632,10 @@ def test_eval_return_ok(self): val = self.redis.eval('return redis.status_reply("Testing")', 0) self.assertEqual(val, b'Testing') + def test_eval_return_ok_wrong_type(self): + with self.assertRaises(redis.ResponseError): + self.redis.eval('return redis.status_reply(123)', 0) + class TestFakeRedis(unittest.TestCase): decode_responses = False From b426f0fe0516a33fd84b1fb506b12c337456d910 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:21:43 -0500 Subject: [PATCH 29/50] pcall --- fakeredis.py | 41 ++++++++++++++++++++++++++--------------- test_fakeredis.py | 16 ++++++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index 73460cb..4a806a6 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -648,9 +648,10 @@ def eval(self, script, numkeys, *keys_and_args): set_globals = lua_runtime.eval( """ - function(keys, argv, callback) + function(keys, argv, redis_call, redis_pcall) redis = {{}} - redis.call = callback + redis.call = redis_call + redis.pcall = redis_pcall redis.error_reply = function(msg) return {err=msg} end redis.status_reply = function(msg) return {ok=msg} end KEYS = keys @@ -662,7 +663,8 @@ def eval(self, script, numkeys, *keys_and_args): set_globals( (None,) + keys_and_args[:numkeys], (None,) + keys_and_args[numkeys:], - functools.partial(self._lua_callback, lua_runtime, expected_globals) + functools.partial(self._lua_redis_call, lua_runtime, expected_globals), + functools.partial(self._lua_redis_pcall, lua_runtime, expected_globals) ) expected_globals.update(lua_runtime.globals().keys()) @@ -679,17 +681,18 @@ def _decode_lua_result(self, result, nested=True): from lupa import lua_type if lua_type(result) == 'table': - if not nested: - for key in ('ok', 'err'): - if key in result: - msg = result[key] - if not isinstance(msg, str): - raise ResponseError("wrong number or type of arguments") - decoded = self._decode_lua_result(msg) - if key == 'ok': - return decoded - else: - raise ResponseError(decoded) + for key in ('ok', 'err'): + if key in result: + msg = result[key] + if not isinstance(msg, str): + raise ResponseError("wrong number or type of arguments") + decoded = self._decode_lua_result(msg) + if key == 'ok': + return decoded + elif nested: + return ResponseError(decoded) + else: + raise ResponseError(decoded) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in count(1): @@ -715,7 +718,15 @@ def _check_for_lua_globals(self, lua_runtime, expected_globals): ) ) - def _lua_callback(self, lua_runtime, expected_globals, op, *args): + def _lua_redis_pcall(self, lua_runtime, expected_globals, op, *args): + try: + return self._lua_redis_call(lua_runtime, expected_globals, op, *args) + except Exception as ex: + return lua_runtime.table_from( + {"err": str(ex)} + ) + + def _lua_redis_call(self, lua_runtime, expected_globals, op, *args): # Check if we've set any global variables before making any change. self._check_for_lua_globals(lua_runtime, expected_globals) # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a diff --git a/test_fakeredis.py b/test_fakeredis.py index 2c54886..07b6e33 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2636,6 +2636,22 @@ def test_eval_return_ok_wrong_type(self): with self.assertRaises(redis.ResponseError): self.redis.eval('return redis.status_reply(123)', 0) + def test_pcall(self): + val = self.redis.eval( + ''' + local a = {} + a[1] = redis.pcall("foo") + return a + ''', + 0 + ) + self.assertIsInstance(val, list) + self.assertEqual(len(val), 1) + self.assertIsInstance(val[0], ResponseError) + + def test_pcall_return_value(self): + with self.assertRaises(ResponseError): + val = self.redis.eval('return redis.pcall("foo")', 0) class TestFakeRedis(unittest.TestCase): decode_responses = False From 6153787024b56c8b98c09b60e7200eb6a2de828d Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:24:01 -0500 Subject: [PATCH 30/50] Test nested ok response --- test_fakeredis.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test_fakeredis.py b/test_fakeredis.py index 07b6e33..3fa2b9f 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2632,6 +2632,17 @@ def test_eval_return_ok(self): val = self.redis.eval('return redis.status_reply("Testing")', 0) self.assertEqual(val, b'Testing') + def test_eval_return_ok_nested(self): + val = self.redis.eval( + ''' + local a = {} + a[1] = {ok="Testing"} + return a + ''', + 0 + ) + self.assertEqual(val, [b'Testing']) + def test_eval_return_ok_wrong_type(self): with self.assertRaises(redis.ResponseError): self.redis.eval('return redis.status_reply(123)', 0) From 7fb69f4212ca64f7c9021f7c49ad5badde8ffb85 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 12:29:12 -0500 Subject: [PATCH 31/50] pep8 --- fakeredis.py | 2 +- test_fakeredis.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index 4a806a6..0ab44b1 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -642,7 +642,7 @@ def eval(self, script, numkeys, *keys_and_args): In practice, use the object returned by ``register_script``. This function exists purely for Redis API completion. """ - from lupa import LuaRuntime, lua_type, LuaSyntaxError + from lupa import LuaRuntime, LuaSyntaxError lua_runtime = LuaRuntime(unpack_returned_tuples=True) diff --git a/test_fakeredis.py b/test_fakeredis.py index 3fa2b9f..e43faac 100644 --- a/test_fakeredis.py +++ b/test_fakeredis.py @@ -2662,7 +2662,8 @@ def test_pcall(self): def test_pcall_return_value(self): with self.assertRaises(ResponseError): - val = self.redis.eval('return redis.pcall("foo")', 0) + self.redis.eval('return redis.pcall("foo")', 0) + class TestFakeRedis(unittest.TestCase): decode_responses = False From 392206c0482380cd148a0704e926a9fbf147f2ab Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 13:04:23 -0500 Subject: [PATCH 32/50] Remove extra braces --- fakeredis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fakeredis.py b/fakeredis.py index 0ab44b1..77972fc 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -12,6 +12,7 @@ import time import types import re +import functools from itertools import count import redis @@ -649,7 +650,7 @@ def eval(self, script, numkeys, *keys_and_args): set_globals = lua_runtime.eval( """ function(keys, argv, redis_call, redis_pcall) - redis = {{}} + redis = {} redis.call = redis_call redis.pcall = redis_pcall redis.error_reply = function(msg) return {err=msg} end From fe9065d4dd524b4bbbc0c66a5f8f86f936792807 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 13:14:58 -0500 Subject: [PATCH 33/50] Fix type --- fakeredis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fakeredis.py b/fakeredis.py index 77972fc..d646a9e 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -685,7 +685,7 @@ def _decode_lua_result(self, result, nested=True): for key in ('ok', 'err'): if key in result: msg = result[key] - if not isinstance(msg, str): + if not isinstance(msg, bytes): raise ResponseError("wrong number or type of arguments") decoded = self._decode_lua_result(msg) if key == 'ok': From 296c2ff90819f913fb8c013db385c116de922502 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 13:18:30 -0500 Subject: [PATCH 34/50] Fix decoding --- fakeredis.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index d646a9e..428b039 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -684,16 +684,15 @@ def _decode_lua_result(self, result, nested=True): if lua_type(result) == 'table': for key in ('ok', 'err'): if key in result: - msg = result[key] + msg = self._decode_lua_result(result[key]) if not isinstance(msg, bytes): raise ResponseError("wrong number or type of arguments") - decoded = self._decode_lua_result(msg) if key == 'ok': - return decoded + return msg elif nested: - return ResponseError(decoded) + return ResponseError(msg) else: - raise ResponseError(decoded) + raise ResponseError(msg) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in count(1): From fde2b75c0ed2ef2da9d9b36a254c17beda690b3b Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 13:24:27 -0500 Subject: [PATCH 35/50] Mess with string types some more --- fakeredis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fakeredis.py b/fakeredis.py index 428b039..0e08a3a 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -701,7 +701,7 @@ def _decode_lua_result(self, result, nested=True): item = result[index] result_list.append(self._decode_lua_result(item)) return result_list - elif isinstance(result, str) and not isinstance(result, bytes): + elif isinstance(result, getattr(__builtins__, 'unicode', str)): return result.encode() elif isinstance(result, float): return int(result) From b1a0402ecc0115a4dde9cad07ee48e5d69f1a600 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 13:36:09 -0500 Subject: [PATCH 36/50] Fix test --- fakeredis.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fakeredis.py b/fakeredis.py index 0e08a3a..9a86c6b 100644 --- a/fakeredis.py +++ b/fakeredis.py @@ -680,7 +680,6 @@ def eval(self, script, numkeys, *keys_and_args): def _decode_lua_result(self, result, nested=True): from lupa import lua_type - if lua_type(result) == 'table': for key in ('ok', 'err'): if key in result: @@ -701,7 +700,7 @@ def _decode_lua_result(self, result, nested=True): item = result[index] result_list.append(self._decode_lua_result(item)) return result_list - elif isinstance(result, getattr(__builtins__, 'unicode', str)): + elif isinstance(result, type(u'')): return result.encode() elif isinstance(result, float): return int(result) From acc44b109292012915daca0cb1f403a7a6fe0a50 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Thu, 8 Feb 2018 17:20:16 -0500 Subject: [PATCH 37/50] Test for nil in table --- test_fakenewsredis.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 74712a5..449ead2 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3090,6 +3090,17 @@ def test_eval_table(self): val = self.redis.eval(lua, 0) self.assertEqual(val, [b'foo', b'bar']) + def test_eval_table_with_nil(self): + lua = """ + local a = {} + a[1] = "foo" + a[2] = nil + a[3] = "bar" + return a + """ + val = self.redis.eval(lua, 0) + self.assertEqual(val, [b'foo']) + def test_eval_table_with_numbers(self): lua = """ local a = {} @@ -3182,7 +3193,7 @@ def test_eval_return_ok_wrong_type(self): with self.assertRaises(redis.ResponseError): self.redis.eval('return redis.status_reply(123)', 0) - def test_pcall(self): + def test_eval_pcall(self): val = self.redis.eval( ''' local a = {} @@ -3195,7 +3206,7 @@ def test_pcall(self): self.assertEqual(len(val), 1) self.assertIsInstance(val[0], ResponseError) - def test_pcall_return_value(self): + def test_eval_pcall_return_value(self): with self.assertRaises(ResponseError): self.redis.eval('return redis.pcall("foo")', 0) From 23a43af168122d2bc9a4c2cde1401017b2517c87 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Fri, 9 Feb 2018 10:06:04 -0500 Subject: [PATCH 38/50] More tests --- fakenewsredis.py | 19 ++++++++++++++----- test_fakenewsredis.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index bdb1b2c..7cf6f40 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -743,14 +743,23 @@ def eval(self, script, numkeys, *keys_and_args): self._check_for_lua_globals(lua_runtime, expected_globals) - return self._decode_lua_result(result, nested=False) + return self._convert_lua_result(result, nested=False) - def _decode_lua_result(self, result, nested=True): + def _convert_redis_result(self, result): + if isinstance(result, dict): + return [ + i + for item in result.items() + for i in item + ] + return result + + def _convert_lua_result(self, result, nested=True): from lupa import lua_type if lua_type(result) == 'table': for key in ('ok', 'err'): if key in result: - msg = self._decode_lua_result(result[key]) + msg = self._convert_lua_result(result[key]) if not isinstance(msg, bytes): raise ResponseError("wrong number or type of arguments") if key == 'ok': @@ -765,7 +774,7 @@ def _decode_lua_result(self, result, nested=True): if index not in result: break item = result[index] - result_list.append(self._decode_lua_result(item)) + result_list.append(self._convert_lua_result(item)) return result_list elif isinstance(result, type(u'')): return result.encode() @@ -824,7 +833,7 @@ def _lua_redis_call(self, lua_runtime, expected_globals, op, *args): op = op.lower() func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) try: - return func(self, *args) + return self._convert_redis_result(func(self, *args)) except Exception as ex: raise ResponseError(ex) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 449ead2..f38b6c7 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3120,6 +3120,41 @@ def test_eval_nested_table(self): val = self.redis.eval(lua, 0) self.assertEqual(val, [[b'foo']]) + def test_eval_mget(self): + self.redis.set('{foo}1', 'bar1') + self.redis.set('{foo}2', 'bar2') + val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, 'foo1', 'foo2') + self.assertEqual(val, [b'bar1', b'bar2']) + + def test_eval_mget_none(self): + self.redis.set('{foo}1', None) + self.redis.set('{foo}2', None) + val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, 'foo1', 'foo2') + self.assertEqual(val, [b'None', b'None']) + + def test_eval_mget_not_set(self): + val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, 'foo1', 'foo2') + self.assertEqual(val, [None, None]) + + def test_eval_hgetall(self): + self.redis.hset('foo', 'k1', 'bar') + self.redis.hset('foo', 'k2', 'baz') + val = self.redis.eval('return redis.call("hgetall", "foo")', 1, 'foo') + self.assertIn( + val, + [ + [b'k1', b'bar', b'k2', b'baz'], + [b'k2', b'baz', b'k1', b'bar'] + ] + ) + + def test_eval_list_with_nil(self): + self.redis.lpush('foo', 'bar') + self.redis.lpush('foo', None) + self.redis.lpush('foo', 'baz') + val = self.redis.eval('return redis.call("lrange", KEYS[1], 0, 2)', 1, 'foo') + self.assertEqual(val, [b'baz', b'None', b'bar']) + def test_eval_invalid_command(self): with self.assertRaises(ResponseError): self.redis.eval( @@ -3164,6 +3199,10 @@ def test_eval_convert_bool(self): self.assertEqual(val, 1) self.assertNotIsInstance(val, bool) + def test_eval_convert_nil_to_false(self): + val = self.redis.eval('return ARGV[1] == false', 0, None) + self.assertFalse(val) + def test_eval_return_error(self): with self.assertRaises(redis.ResponseError) as cm: self.redis.eval('return {err="Testing"}', 0) From 525a29b5088aebfe1411b5818717603ac813e73a Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Fri, 9 Feb 2018 10:28:48 -0500 Subject: [PATCH 39/50] Fix test --- fakenewsredis.py | 2 +- test_fakenewsredis.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index 7cf6f40..b34a077 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -777,7 +777,7 @@ def _convert_lua_result(self, result, nested=True): result_list.append(self._convert_lua_result(item)) return result_list elif isinstance(result, type(u'')): - return result.encode() + return to_bytes(result) elif isinstance(result, float): return int(result) elif isinstance(result, bool): diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index f38b6c7..2ed0197 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3140,13 +3140,16 @@ def test_eval_hgetall(self): self.redis.hset('foo', 'k1', 'bar') self.redis.hset('foo', 'k2', 'baz') val = self.redis.eval('return redis.call("hgetall", "foo")', 1, 'foo') - self.assertIn( - val, + sorted_val = sorted( [ - [b'k1', b'bar', b'k2', b'baz'], - [b'k2', b'baz', b'k1', b'bar'] + val[:2], + val[2:] ] ) + self.assertEqual( + sorted_val, + [[b'k1', b'bar'], [b'k2', b'baz']] + ) def test_eval_list_with_nil(self): self.redis.lpush('foo', 'bar') From 267e33805e5955af7bebddb6c41334d870a26eef Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Fri, 9 Feb 2018 14:36:37 -0500 Subject: [PATCH 40/50] Remove unnecessary exception handler --- fakenewsredis.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index b34a077..a4afbe8 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -832,10 +832,7 @@ def _lua_redis_call(self, lua_runtime, expected_globals, op, *args): } op = op.lower() func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) - try: - return self._convert_redis_result(func(self, *args)) - except Exception as ex: - raise ResponseError(ex) + return self._convert_redis_result(func(self, *args)) def _retrive_data_from_sort(self, data, get): if get is not None: From 6377669c463ce4f8be5368a6cc5249ae60661e21 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 09:11:41 -0500 Subject: [PATCH 41/50] WIP: fallout --- fakenewsredis.py | 45 +++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index a4afbe8..1052357 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -797,40 +797,41 @@ def _lua_redis_pcall(self, lua_runtime, expected_globals, op, *args): try: return self._lua_redis_call(lua_runtime, expected_globals, op, *args) except Exception as ex: - return lua_runtime.table_from( - {"err": str(ex)} - ) + return lua_runtime.table_from({"err": str(ex)}) def _lua_redis_call(self, lua_runtime, expected_globals, op, *args): # Check if we've set any global variables before making any change. self._check_for_lua_globals(lua_runtime, expected_globals) - # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect a - # ResponseError for consistency with Redis + # These commands aren't necessarily all implemented, but if op is not one of these commands, we expect + # a ResponseError for consistency with Redis commands = [ - 'APPEND', 'AUTH', 'BITCOUNT', 'BITFIELD', 'BITOP', 'BITPOS', 'BLPOP', 'BRPOP', 'BRPOPLPUSH', 'DECR', - 'DECRBY', 'DEL', 'DUMP', 'ECHO', 'EVAL', 'EVALSHA', 'EXISTS', 'EXPIRE', 'EXPIREAT', 'FLUSHALL', 'FLUSHDB', - 'GEOADD', 'GEODIST', 'GEOHASH', 'GEOPOS', 'GEORADIUS', 'GEORADIUSBYMEMBER', 'GET', 'GETBIT', 'GETRANGE', - 'GETSET', 'HDEL', 'HEXISTS', 'HGET', 'HGETALL', 'HINCRBY', 'HINCRBYFLOAT', 'HKEYS', 'HLEN', 'HMGET', - 'HMSET', 'HSCAN', 'HSET', 'HSETNX', 'HSTRLEN', 'HVALS', 'INCR', 'INCRBY', 'INCRBYFLOAT', 'INFO', 'KEYS', - 'LINDEX', 'LINSERT', 'LLEN', 'LPOP', 'LPUSH', 'LPUSHX', 'LRANGE', 'LREM', 'LSET', 'LTRIM', 'MGET', - 'MIGRATE', 'MOVE', 'MSET', 'MSETNX', 'OBJECT', 'PERSIST', 'PEXPIRE', 'PEXPIREAT', 'PFADD', 'PFCOUNT', - 'PFMERGE', 'PING', 'PSETEX', 'PSUBSCRIBE', 'PTTL', 'PUBLISH', 'PUBSUB', 'PUNSUBSCRIBE', 'RENAME', - 'RENAMENX', 'RESTORE', 'RPOP', 'RPOPLPUSH', 'RPUSH', 'RPUSHX', 'SADD', 'SCAN', 'SCARD', 'SDIFF', - 'SDIFFSTORE', 'SELECT', 'SET', 'SETBIT', 'SETEX', 'SETNX', 'SETRANGE', 'SHUTDOWN', 'SINTER', 'SINTERSTORE', - 'SISMEMBER', 'SLAVEOF', 'SLOWLOG', 'SMEMBERS', 'SMOVE', 'SORT', 'SPOP', 'SRANDMEMBER', 'SREM', 'SSCAN', - 'STRLEN', 'SUBSCRIBE', 'SUNION', 'SUNIONSTORE', 'SWAPDB', 'TOUCH', 'TTL', 'TYPE', 'UNLINK', 'UNSUBSCRIBE', - 'WAIT', 'WATCH', 'ZADD', 'ZCARD', 'ZCOUNT', 'ZINCRBY', 'ZINTERSTORE', 'ZLEXCOUNT', 'ZRANGE', 'ZRANGEBYLEX', - 'ZRANGEBYSCORE', 'ZRANK', 'ZREM', 'ZREMRANGEBYLEX', 'ZREMRANGEBYRANK', 'ZREMRANGEBYSCORE', 'ZREVRANGE', - 'ZREVRANGEBYLEX', 'ZREVRANGEBYSCORE', 'ZREVRANK', 'ZSCAN', 'ZSCORE', 'ZUNIONSTORE' + 'append', 'auth', 'bitcount', 'bitfield', 'bitop', 'bitpos', 'blpop', 'brpop', 'brpoplpush', + 'decr', 'decrby', 'del', 'dump', 'echo', 'eval', 'evalsha', 'exists', 'expire', 'expireat', + 'flushall', 'flushdb', 'geoadd', 'geodist', 'geohash', 'geopos', 'georadius', 'georadiusbymember', + 'get', 'getbit', 'getrange', 'getset', 'hdel', 'hexists', 'hget', 'hgetall', 'hincrby', + 'hincrbyfloat', 'hkeys', 'hlen', 'hmget', 'hmset', 'hscan', 'hset', 'hsetnx', 'hstrlen', 'hvals', + 'incr', 'incrby', 'incrbyfloat', 'info', 'keys', 'lindex', 'linsert', 'llen', 'lpop', 'lpush', + 'lpushx', 'lrange', 'lrem', 'lset', 'ltrim', 'mget', 'migrate', 'move', 'mset', 'msetnx', + 'object', 'persist', 'pexpire', 'pexpireat', 'pfadd', 'pfcount', 'pfmerge', 'ping', 'psetex', + 'psubscribe', 'pttl', 'publish', 'pubsub', 'punsubscribe', 'rename', 'renamenx', 'restore', + 'rpop', 'rpoplpush', 'rpush', 'rpushx', 'sadd', 'scan', 'scard', 'sdiff', 'sdiffstore', 'select', + 'set', 'setbit', 'setex', 'setnx', 'setrange', 'shutdown', 'sinter', 'sinterstore', 'sismember', + 'slaveof', 'slowlog', 'smembers', 'smove', 'sort', 'spop', 'srandmember', 'srem', 'sscan', + 'strlen', 'subscribe', 'sunion', 'sunionstore', 'swapdb', 'touch', 'ttl', 'type', 'unlink', + 'unsubscribe', 'wait', 'watch', 'zadd', 'zcard', 'zcount', 'zincrby', 'zinterstore', 'zlexcount', + 'zrange', 'zrangebylex', 'zrangebyscore', 'zrank', 'zrem', 'zremrangebylex', 'zremrangebyrank', + 'zremrangebyscore', 'zrevrange', 'zrevrangebylex', 'zrevrangebyscore', 'zrevrank', 'zscan', + 'zscore', 'zunionstore' ] - if op.upper() not in commands: + + op = op.lower() + if op not in commands: raise ResponseError("Unknown Redis command called from Lua script") special_cases = { 'del': FakeStrictRedis.delete, 'decrby': FakeStrictRedis.decr, 'incrby': FakeStrictRedis.incr } - op = op.lower() func = special_cases[op] if op in special_cases else getattr(FakeStrictRedis, op) return self._convert_redis_result(func(self, *args)) From 244056bc8e63edbd563466a69b331263fe8866cc Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 09:12:44 -0500 Subject: [PATCH 42/50] Remove docstring --- fakenewsredis.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index 1052357..fbd9b6e 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -703,13 +703,6 @@ def sort(self, name, start=None, num=None, by=None, get=None, desc=False, return [] def eval(self, script, numkeys, *keys_and_args): - """ - Execute the Lua ``script``, specifying the ``numkeys`` the script - will touch and the key names and argument values in ``keys_and_args``. - Returns the result of the script. - In practice, use the object returned by ``register_script``. This - function exists purely for Redis API completion. - """ from lupa import LuaRuntime, LuaSyntaxError lua_runtime = LuaRuntime(unpack_returned_tuples=True) From 8f869b4f9970ccf6e7c8098238873c5d9c5861b3 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 09:18:10 -0500 Subject: [PATCH 43/50] More fallout --- README.rst | 5 ++--- test_fakenewsredis.py | 7 +------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index f7dd566..18c894b 100644 --- a/README.rst +++ b/README.rst @@ -265,10 +265,9 @@ they have all been tagged as 'slow' so you can skip them by running:: Revision history ================ -0.9.5 +Development version ----- -Add support for StrictRedis.eval for Lua scripts -- `#9 `_ +- `#9 `_ Add support for StrictRedis.eval for Lua scripts 0.9.4 ----- diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 2ed0197..1c498c5 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3140,12 +3140,7 @@ def test_eval_hgetall(self): self.redis.hset('foo', 'k1', 'bar') self.redis.hset('foo', 'k2', 'baz') val = self.redis.eval('return redis.call("hgetall", "foo")', 1, 'foo') - sorted_val = sorted( - [ - val[:2], - val[2:] - ] - ) + sorted_val = sorted([val[:2], val[2:]]) self.assertEqual( sorted_val, [[b'k1', b'bar'], [b'k2', b'baz']] From b2b67867c4b1cb231ddf29a17e40bc7bcf981d15 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 09:24:59 -0500 Subject: [PATCH 44/50] Catch LuaError --- fakenewsredis.py | 4 ++-- test_fakenewsredis.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index fbd9b6e..5eaac5e 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -703,7 +703,7 @@ def sort(self, name, start=None, num=None, by=None, get=None, desc=False, return [] def eval(self, script, numkeys, *keys_and_args): - from lupa import LuaRuntime, LuaSyntaxError + from lupa import LuaRuntime, LuaError lua_runtime = LuaRuntime(unpack_returned_tuples=True) @@ -731,7 +731,7 @@ def eval(self, script, numkeys, *keys_and_args): try: result = lua_runtime.execute(script) - except LuaSyntaxError as ex: + except LuaError as ex: raise ResponseError(ex) self._check_for_lua_globals(lua_runtime, expected_globals) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 1c498c5..356d64b 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3164,6 +3164,10 @@ def test_eval_syntax_error(self): with self.assertRaises(ResponseError): self.redis.eval('return "', 0) + def test_eval_runtime_error(self): + with self.assertRaises(ResponseError): + self.redis.eval('error("CRASH")', 0) + def test_eval_global_variable(self): # Redis doesn't allow script to define global variables with self.assertRaises(ResponseError): From fa252bc543728db43d86b638eb3b497e42ae9292 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 09:37:01 -0500 Subject: [PATCH 45/50] Check number of keys --- fakenewsredis.py | 3 +++ test_fakenewsredis.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/fakenewsredis.py b/fakenewsredis.py index 5eaac5e..2b0d1f3 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -705,6 +705,9 @@ def sort(self, name, start=None, num=None, by=None, get=None, desc=False, def eval(self, script, numkeys, *keys_and_args): from lupa import LuaRuntime, LuaError + if numkeys > len(keys_and_args): + raise ResponseError("Number of keys can't be greater than number of args") + lua_runtime = LuaRuntime(unpack_returned_tuples=True) set_globals = lua_runtime.eval( diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 356d64b..ba1f30d 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3168,6 +3168,10 @@ def test_eval_runtime_error(self): with self.assertRaises(ResponseError): self.redis.eval('error("CRASH")', 0) + def test_more_keys_than_args(self): + with self.assertRaises(ResponseError): + self.redis.eval('return 1', 42) + def test_eval_global_variable(self): # Redis doesn't allow script to define global variables with self.assertRaises(ResponseError): From f917498621d4b000ef8974f4192055f5e8f5445e Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 09:41:01 -0500 Subject: [PATCH 46/50] More fallout --- fakenewsredis.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index 2b0d1f3..482241d 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -772,12 +772,12 @@ def _convert_lua_result(self, result, nested=True): item = result[index] result_list.append(self._convert_lua_result(item)) return result_list - elif isinstance(result, type(u'')): + elif isinstance(result, text_type): return to_bytes(result) elif isinstance(result, float): return int(result) elif isinstance(result, bool): - return result and 1 or None + return 1 if result else None return result def _check_for_lua_globals(self, lua_runtime, expected_globals): diff --git a/setup.py b/setup.py index 56224f2..36b76c4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='fakenewsredis', - version='0.9.5', + version='0.9.4', description="Fake implementation of redis API for testing purposes.", long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), From 82e7e65d1855c1c1d6e666f215a3a513f7493fbd Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 10:04:06 -0500 Subject: [PATCH 47/50] More edge cases for numkeys --- fakenewsredis.py | 14 +++++++++++++- test_fakenewsredis.py | 22 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index 482241d..51fc045 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -705,8 +705,20 @@ def sort(self, name, start=None, num=None, by=None, get=None, desc=False, def eval(self, script, numkeys, *keys_and_args): from lupa import LuaRuntime, LuaError - if numkeys > len(keys_and_args): + if any( + isinstance(numkeys, t) for t in (text_type, str, bytes) + ): + try: + numkeys = int(numkeys) + except ValueError: + # Non-numeric string will be handled below. + pass + if not(isinstance(numkeys, int)): + raise ResponseError("value is not an integer or out of range") + elif numkeys > len(keys_and_args): raise ResponseError("Number of keys can't be greater than number of args") + elif numkeys < 0: + raise ResponseError("Number of keys can't be negative") lua_runtime = LuaRuntime(unpack_returned_tuples=True) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index ba1f30d..5cfd119 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3123,17 +3123,17 @@ def test_eval_nested_table(self): def test_eval_mget(self): self.redis.set('{foo}1', 'bar1') self.redis.set('{foo}2', 'bar2') - val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, 'foo1', 'foo2') + val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, '{foo}1', '{foo}2') self.assertEqual(val, [b'bar1', b'bar2']) def test_eval_mget_none(self): self.redis.set('{foo}1', None) self.redis.set('{foo}2', None) - val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, 'foo1', 'foo2') + val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, '{foo}1', '{foo}2') self.assertEqual(val, [b'None', b'None']) def test_eval_mget_not_set(self): - val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, 'foo1', 'foo2') + val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, '{foo}1', '{foo}2') self.assertEqual(val, [None, None]) def test_eval_hgetall(self): @@ -3172,6 +3172,22 @@ def test_more_keys_than_args(self): with self.assertRaises(ResponseError): self.redis.eval('return 1', 42) + def test_numkeys_float_string(self): + with self.assertRaises(ResponseError): + val = self.redis.eval('return KEYS[1]', '0.7', 'foo') + + def test_numkeys_integer_string(self): + val = self.redis.eval('return KEYS[1]', "1", "foo") + self.assertEqual(val, "foo") + + def test_numkeys_negative(self): + with self.assertRaises(ResponseError): + self.redis.eval('return KEYS[1]', -1, "foo") + + def test_numkeys_float(self): + with self.assertRaises(ResponseError): + self.redis.eval('return KEYS[1]', 0.7, "foo") + def test_eval_global_variable(self): # Redis doesn't allow script to define global variables with self.assertRaises(ResponseError): From fdf9ecb75df125e48545e90bd32460ce5873e3bc Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 10:07:19 -0500 Subject: [PATCH 48/50] Remove braces --- test_fakenewsredis.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 5cfd119..ae33486 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3121,19 +3121,19 @@ def test_eval_nested_table(self): self.assertEqual(val, [[b'foo']]) def test_eval_mget(self): - self.redis.set('{foo}1', 'bar1') - self.redis.set('{foo}2', 'bar2') - val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, '{foo}1', '{foo}2') + self.redis.set('foo1', 'bar1') + self.redis.set('foo2', 'bar2') + val = self.redis.eval('return redis.call("mget", "foo1", "foo2")', 2, 'foo1', 'foo2') self.assertEqual(val, [b'bar1', b'bar2']) def test_eval_mget_none(self): - self.redis.set('{foo}1', None) - self.redis.set('{foo}2', None) - val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, '{foo}1', '{foo}2') + self.redis.set('foo1', None) + self.redis.set('foo2', None) + val = self.redis.eval('return redis.call("mget", "foo1", "foo2")', 2, 'foo1', 'foo2') self.assertEqual(val, [b'None', b'None']) def test_eval_mget_not_set(self): - val = self.redis.eval('return redis.call("mget", "{foo}1", "{foo}2")', 2, '{foo}1', '{foo}2') + val = self.redis.eval('return redis.call("mget", "foo1", "foo2")', 2, 'foo1', 'foo2') self.assertEqual(val, [None, None]) def test_eval_hgetall(self): From a6a7cd67228cfe1d0ddc7fa2e55a9b425b2f4908 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 10:09:57 -0500 Subject: [PATCH 49/50] flake8 --- test_fakenewsredis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index ae33486..64bbbbc 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3174,7 +3174,7 @@ def test_more_keys_than_args(self): def test_numkeys_float_string(self): with self.assertRaises(ResponseError): - val = self.redis.eval('return KEYS[1]', '0.7', 'foo') + self.redis.eval('return KEYS[1]', '0.7', 'foo') def test_numkeys_integer_string(self): val = self.redis.eval('return KEYS[1]', "1", "foo") From 9f4d9105936e754763a9fb73b4ff9d99e19d6c57 Mon Sep 17 00:00:00 2001 From: Blake Foster Date: Mon, 12 Feb 2018 10:20:13 -0500 Subject: [PATCH 50/50] to_bytes --- fakenewsredis.py | 5 +++-- test_fakenewsredis.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/fakenewsredis.py b/fakenewsredis.py index 51fc045..101b816 100644 --- a/fakenewsredis.py +++ b/fakenewsredis.py @@ -720,6 +720,7 @@ def eval(self, script, numkeys, *keys_and_args): elif numkeys < 0: raise ResponseError("Number of keys can't be negative") + keys_and_args = [to_bytes(v) for v in keys_and_args] lua_runtime = LuaRuntime(unpack_returned_tuples=True) set_globals = lua_runtime.eval( @@ -737,8 +738,8 @@ def eval(self, script, numkeys, *keys_and_args): ) expected_globals = set() set_globals( - (None,) + keys_and_args[:numkeys], - (None,) + keys_and_args[numkeys:], + [None] + keys_and_args[:numkeys], + [None] + keys_and_args[numkeys:], functools.partial(self._lua_redis_call, lua_runtime, expected_globals), functools.partial(self._lua_redis_pcall, lua_runtime, expected_globals) ) diff --git a/test_fakenewsredis.py b/test_fakenewsredis.py index 64bbbbc..e6ede44 100644 --- a/test_fakenewsredis.py +++ b/test_fakenewsredis.py @@ -3178,7 +3178,7 @@ def test_numkeys_float_string(self): def test_numkeys_integer_string(self): val = self.redis.eval('return KEYS[1]', "1", "foo") - self.assertEqual(val, "foo") + self.assertEqual(val, b'foo') def test_numkeys_negative(self): with self.assertRaises(ResponseError): @@ -3221,9 +3221,9 @@ def test_eval_convert_bool(self): self.assertEqual(val, 1) self.assertNotIsInstance(val, bool) - def test_eval_convert_nil_to_false(self): - val = self.redis.eval('return ARGV[1] == false', 0, None) - self.assertFalse(val) + def test_eval_none_arg(self): + val = self.redis.eval('return ARGV[1] == "None"', 0, None) + self.assertTrue(val) def test_eval_return_error(self): with self.assertRaises(redis.ResponseError) as cm: