diff options
author | dvora-h <67596500+dvora-h@users.noreply.github.com> | 2022-02-22 13:11:55 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-22 13:11:55 +0200 |
commit | fa76ac49a9ea02c204bd4f1644f39d90140cf356 (patch) | |
tree | 0578d5e4748c64824a1f72ed3b64f23c8f57368b | |
parent | f2e34739fccab28a28a066a3ece955eb455b32f9 (diff) | |
download | redis-py-fa76ac49a9ea02c204bd4f1644f39d90140cf356.tar.gz |
Add support for Redis 7 functions (#1998)
* add function support
* linters
* test fcall
* decode reponses for unstable_r
* linters
* fix evalsho_ro test
* fix eval_ro test
* add response callbaks
* linters
-rwxr-xr-x | redis/client.py | 4 | ||||
-rw-r--r-- | redis/commands/core.py | 126 | ||||
-rwxr-xr-x | redis/connection.py | 11 | ||||
-rw-r--r-- | tests/conftest.py | 4 | ||||
-rw-r--r-- | tests/test_commands.py | 22 | ||||
-rw-r--r-- | tests/test_function.py | 94 | ||||
-rw-r--r-- | tests/test_scripting.py | 4 |
7 files changed, 249 insertions, 16 deletions
diff --git a/redis/client.py b/redis/client.py index 22c5dc1..0eade79 100755 --- a/redis/client.py +++ b/redis/client.py @@ -733,6 +733,10 @@ class AbstractRedis: "CONFIG RESETSTAT": bool_ok, "CONFIG SET": bool_ok, "DEBUG OBJECT": parse_debug_object, + "FUNCTION DELETE": bool_ok, + "FUNCTION FLUSH": bool_ok, + "FUNCTION LOAD": bool_ok, + "FUNCTION RESTORE": bool_ok, "GEOHASH": lambda r: list(map(str_if_bytes, r)), "GEOPOS": lambda r: list( map(lambda ll: (float(ll[0]), float(ll[1])) if ll is not None else None, r) diff --git a/redis/commands/core.py b/redis/commands/core.py index 80bc55f..e74550f 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -5429,6 +5429,131 @@ class ClusterCommands(CommandsProtocol): return self.execute_command("READONLY", **kwargs) +class FunctionCommands: + """ + Redis Function commands + """ + + def function_load( + self, + engine: str, + library: str, + code: str, + replace: Optional[bool] = False, + description: Optional[str] = None, + ) -> str: + """ + Load a library to Redis. + :param engine: the name of the execution engine for the library + :param library: the unique name of the library + :param code: the source code + :param replace: changes the behavior to replace the library if a library called + ``library`` already exists + :param description: description to the library + + For more information check https://redis.io/commands/function-load + """ + pieces = [engine, library] + if replace: + pieces.append("REPLACE") + if description is not None: + pieces.append(description) + pieces.append(code) + return self.execute_command("FUNCTION LOAD", *pieces) + + def function_delete(self, library: str) -> str: + """ + Delete the library called ``library`` and all its functions. + + For more information check https://redis.io/commands/function-delete + """ + return self.execute_command("FUNCTION DELETE", library) + + def function_flush(self, mode: str = "SYNC") -> str: + """ + Deletes all the libraries. + + For more information check https://redis.io/commands/function-flush + """ + return self.execute_command("FUNCTION FLUSH", mode) + + def function_list( + self, library: Optional[str] = "*", withcode: Optional[bool] = False + ) -> List: + """ + Return information about the functions and libraries. + :param library: pecify a pattern for matching library names + :param withcode: cause the server to include the libraries source + implementation in the reply + """ + args = ["LIBRARYNAME", library] + if withcode: + args.append("WITHCODE") + return self.execute_command("FUNCTION LIST", *args) + + def _fcall( + self, command: str, function, numkeys: int, *keys_and_args: Optional[List] + ) -> str: + return self.execute_command(command, function, numkeys, *keys_and_args) + + def fcall(self, function, numkeys: int, *keys_and_args: Optional[List]) -> str: + """ + Invoke a function. + + For more information check https://redis.io/commands/fcall + """ + return self._fcall("FCALL", function, numkeys, *keys_and_args) + + def fcall_ro(self, function, numkeys: int, *keys_and_args: Optional[List]) -> str: + """ + This is a read-only variant of the FCALL command that cannot + execute commands that modify data. + + For more information check https://redis.io/commands/fcal_ro + """ + return self._fcall("FCALL_RO", function, numkeys, *keys_and_args) + + def function_dump(self) -> str: + """ + Return the serialized payload of loaded libraries. + + For more information check https://redis.io/commands/function-dump + """ + from redis.client import NEVER_DECODE + + options = {} + options[NEVER_DECODE] = [] + + return self.execute_command("FUNCTION DUMP", **options) + + def function_restore(self, payload: str, policy: Optional[str] = "APPEND") -> str: + """ + Restore libraries from the serialized ``payload``. + You can use the optional policy argument to provide a policy + for handling existing libraries. + + For more information check https://redis.io/commands/function-restore + """ + return self.execute_command("FUNCTION RESTORE", payload, policy) + + def function_kill(self) -> str: + """ + Kill a function that is currently executing. + + For more information check https://redis.io/commands/function-kill + """ + return self.execute_command("FUNCTION KILL") + + def function_stats(self) -> list: + """ + Return information about the function that's currently running + and information about the available execution engines. + + For more information check https://redis.io/commands/function-stats + """ + return self.execute_command("FUNCTION STATS") + + AsyncClusterCommands = ClusterCommands @@ -5474,6 +5599,7 @@ class CoreCommands( ModuleCommands, PubSubCommands, ScriptCommands, + FunctionCommands, ): """ A class containing all of the implemented redis commands. This class is diff --git a/redis/connection.py b/redis/connection.py index 891695d..189cecb 100755 --- a/redis/connection.py +++ b/redis/connection.py @@ -463,10 +463,17 @@ class HiredisParser(BaseParser): self._next_response = False return response - response = self._reader.gets() + if disable_decoding: + response = self._reader.gets(False) + else: + response = self._reader.gets() + while response is False: self.read_from_socket() - response = self._reader.gets() + if disable_decoding: + response = self._reader.gets(False) + else: + response = self._reader.gets() # if an older version of hiredis is installed, we need to attempt # to convert ResponseErrors to their appropriate types. if not HIREDIS_SUPPORTS_CALLABLE_ERRORS: diff --git a/tests/conftest.py b/tests/conftest.py index 2534ca0..b615915 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -434,7 +434,9 @@ def master_host(request): @pytest.fixture() def unstable_r(request): url = request.config.getoption("--redis-unstable-url") - with _get_client(redis.Redis, request, from_url=url) as client: + with _get_client( + redis.Redis, request, from_url=url, decode_responses=True + ) as client: yield client diff --git a/tests/test_commands.py b/tests/test_commands.py index ad9ee2c..5c32d5f 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -612,7 +612,7 @@ class TestRedisCommands: @pytest.mark.onlynoncluster # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_client_no_evict(self, unstable_r): - assert unstable_r.client_no_evict("ON") == b"OK" + assert unstable_r.client_no_evict("ON") == "OK" with pytest.raises(TypeError): unstable_r.client_no_evict() @@ -985,9 +985,9 @@ class TestRedisCommands: # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_lcs(self, unstable_r): unstable_r.mset({"foo": "ohmytext", "bar": "mynewtext"}) - assert unstable_r.lcs("foo", "bar") == b"mytext" + assert unstable_r.lcs("foo", "bar") == "mytext" assert unstable_r.lcs("foo", "bar", len=True) == 6 - result = [b"matches", [[[4, 7], [5, 8]]], b"len", 6] + result = ["matches", [[[4, 7], [5, 8]]], "len", 6] assert unstable_r.lcs("foo", "bar", idx=True, minmatchlen=3) == result with pytest.raises(redis.ResponseError): assert unstable_r.lcs("foo", "bar", len=True, idx=True) @@ -1522,24 +1522,24 @@ class TestRedisCommands: # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_blmpop(self, unstable_r): unstable_r.rpush("a", "1", "2", "3", "4", "5") - res = [b"a", [b"1", b"2"]] + res = ["a", ["1", "2"]] assert unstable_r.blmpop(1, "2", "b", "a", direction="LEFT", count=2) == res with pytest.raises(TypeError): unstable_r.blmpop(1, "2", "b", "a", count=2) unstable_r.rpush("b", "6", "7", "8", "9") - assert unstable_r.blmpop(0, "2", "b", "a", direction="LEFT") == [b"b", [b"6"]] + assert unstable_r.blmpop(0, "2", "b", "a", direction="LEFT") == ["b", ["6"]] assert unstable_r.blmpop(1, "2", "foo", "bar", direction="RIGHT") is None @pytest.mark.onlynoncluster # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_lmpop(self, unstable_r): unstable_r.rpush("foo", "1", "2", "3", "4", "5") - result = [b"foo", [b"1", b"2"]] + result = ["foo", ["1", "2"]] assert unstable_r.lmpop("2", "bar", "foo", direction="LEFT", count=2) == result with pytest.raises(redis.ResponseError): unstable_r.lmpop("2", "bar", "foo", direction="up", count=2) unstable_r.rpush("bar", "a", "b", "c", "d") - assert unstable_r.lmpop("2", "bar", "foo", direction="LEFT") == [b"bar", [b"a"]] + assert unstable_r.lmpop("2", "bar", "foo", direction="LEFT") == ["bar", ["a"]] def test_lindex(self, r): r.rpush("a", "1", "2", "3") @@ -2148,23 +2148,23 @@ class TestRedisCommands: # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_zmpop(self, unstable_r): unstable_r.zadd("a", {"a1": 1, "a2": 2, "a3": 3}) - res = [b"a", [[b"a1", b"1"], [b"a2", b"2"]]] + res = ["a", [["a1", "1"], ["a2", "2"]]] assert unstable_r.zmpop("2", ["b", "a"], min=True, count=2) == res with pytest.raises(redis.DataError): unstable_r.zmpop("2", ["b", "a"], count=2) unstable_r.zadd("b", {"b1": 10, "ab": 9, "b3": 8}) - assert unstable_r.zmpop("2", ["b", "a"], max=True) == [b"b", [[b"b1", b"10"]]] + assert unstable_r.zmpop("2", ["b", "a"], max=True) == ["b", [["b1", "10"]]] @pytest.mark.onlynoncluster # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_bzmpop(self, unstable_r): unstable_r.zadd("a", {"a1": 1, "a2": 2, "a3": 3}) - res = [b"a", [[b"a1", b"1"], [b"a2", b"2"]]] + res = ["a", [["a1", "1"], ["a2", "2"]]] assert unstable_r.bzmpop(1, "2", ["b", "a"], min=True, count=2) == res with pytest.raises(redis.DataError): unstable_r.bzmpop(1, "2", ["b", "a"], count=2) unstable_r.zadd("b", {"b1": 10, "ab": 9, "b3": 8}) - res = [b"b", [[b"b1", b"10"]]] + res = ["b", [["b1", "10"]]] assert unstable_r.bzmpop(0, "2", ["b", "a"], max=True) == res assert unstable_r.bzmpop(1, "2", ["foo", "bar"], max=True) is None diff --git a/tests/test_function.py b/tests/test_function.py new file mode 100644 index 0000000..921ba30 --- /dev/null +++ b/tests/test_function.py @@ -0,0 +1,94 @@ +import pytest + +from redis.exceptions import ResponseError + +function = "redis.register_function('myfunc', function(keys, args) return args[1] end)" +function2 = "redis.register_function('hello', function() return 'Hello World' end)" +set_function = "redis.register_function('set', function(keys, args) \ + return redis.call('SET', keys[1], args[1]) end)" +get_function = "redis.register_function('get', function(keys, args) \ + return redis.call('GET', keys[1]) end)" + + +@pytest.mark.onlynoncluster +# @skip_if_server_version_lt("7.0.0") turn on after redis 7 release +class TestFunction: + @pytest.fixture(autouse=True) + def reset_functions(self, unstable_r): + unstable_r.function_flush() + + def test_function_load(self, unstable_r): + assert unstable_r.function_load("Lua", "mylib", function) + assert unstable_r.function_load("Lua", "mylib", function, replace=True) + with pytest.raises(ResponseError): + unstable_r.function_load("Lua", "mylib", function) + with pytest.raises(ResponseError): + unstable_r.function_load("Lua", "mylib2", function) + + def test_function_delete(self, unstable_r): + unstable_r.function_load("Lua", "mylib", set_function) + with pytest.raises(ResponseError): + unstable_r.function_load("Lua", "mylib", set_function) + assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" + assert unstable_r.function_delete("mylib") + with pytest.raises(ResponseError): + unstable_r.fcall("set", 1, "foo", "bar") + assert unstable_r.function_load("Lua", "mylib", set_function) + + def test_function_flush(self, unstable_r): + unstable_r.function_load("Lua", "mylib", function) + assert unstable_r.fcall("myfunc", 0, "hello") == "hello" + assert unstable_r.function_flush() + with pytest.raises(ResponseError): + unstable_r.fcall("myfunc", 0, "hello") + with pytest.raises(ResponseError): + unstable_r.function_flush("ABC") + + def test_function_list(self, unstable_r): + unstable_r.function_load("Lua", "mylib", function) + res = [ + [ + "library_name", + "mylib", + "engine", + "LUA", + "description", + None, + "functions", + [["name", "myfunc", "description", None]], + ], + ] + assert unstable_r.function_list() == res + assert unstable_r.function_list(library="*lib") == res + assert unstable_r.function_list(withcode=True)[0][9] == function + + def test_fcall(self, unstable_r): + unstable_r.function_load("Lua", "mylib", set_function) + unstable_r.function_load("Lua", "mylib2", get_function) + assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" + assert unstable_r.fcall("get", 1, "foo") == "bar" + with pytest.raises(ResponseError): + unstable_r.fcall("myfunc", 0, "hello") + + def test_fcall_ro(self, unstable_r): + unstable_r.function_load("Lua", "mylib", function) + assert unstable_r.fcall_ro("myfunc", 0, "hello") == "hello" + unstable_r.function_load("Lua", "mylib2", set_function) + with pytest.raises(ResponseError): + unstable_r.fcall_ro("set", 1, "foo", "bar") + + def test_function_dump_restore(self, unstable_r): + unstable_r.function_load("Lua", "mylib", set_function) + payload = unstable_r.function_dump() + assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" + unstable_r.function_delete("mylib") + with pytest.raises(ResponseError): + unstable_r.fcall("set", 1, "foo", "bar") + assert unstable_r.function_restore(payload) + assert unstable_r.fcall("set", 1, "foo", "bar") == "OK" + unstable_r.function_load("Lua", "mylib2", get_function) + assert unstable_r.fcall("get", 1, "foo") == "bar" + unstable_r.function_delete("mylib") + assert unstable_r.function_restore(payload, "FLUSH") + with pytest.raises(ResponseError): + unstable_r.fcall("get", 1, "foo") diff --git a/tests/test_scripting.py b/tests/test_scripting.py index f4671a5..dcf8a78 100644 --- a/tests/test_scripting.py +++ b/tests/test_scripting.py @@ -35,7 +35,7 @@ class TestScripting: # @skip_if_server_version_lt("7.0.0") turn on after redis 7 release def test_eval_ro(self, unstable_r): unstable_r.set("a", "b") - assert unstable_r.eval_ro("return redis.call('GET', KEYS[1])", 1, "a") == b"b" + assert unstable_r.eval_ro("return redis.call('GET', KEYS[1])", 1, "a") == "b" with pytest.raises(redis.ResponseError): unstable_r.eval_ro("return redis.call('DEL', KEYS[1])", 1, "a") @@ -79,7 +79,7 @@ class TestScripting: unstable_r.set("a", "b") get_sha = unstable_r.script_load("return redis.call('GET', KEYS[1])") del_sha = unstable_r.script_load("return redis.call('DEL', KEYS[1])") - assert unstable_r.evalsha_ro(get_sha, 1, "a") == b"b" + assert unstable_r.evalsha_ro(get_sha, 1, "a") == "b" with pytest.raises(redis.ResponseError): unstable_r.evalsha_ro(del_sha, 1, "a") |