Skip to content

Commit a5f6e4b

Browse files
Merge remote-tracking branch 'redis/master'
2 parents 22506e2 + 1f046ac commit a5f6e4b

File tree

6 files changed

+183
-65
lines changed

6 files changed

+183
-65
lines changed

redis/client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,6 @@ class AbstractRedis:
757757
"DEBUG OBJECT": parse_debug_object,
758758
"FUNCTION DELETE": bool_ok,
759759
"FUNCTION FLUSH": bool_ok,
760-
"FUNCTION LOAD": bool_ok,
761760
"FUNCTION RESTORE": bool_ok,
762761
"GEOHASH": lambda r: list(map(str_if_bytes, r)),
763762
"GEOPOS": lambda r: list(

redis/commands/core.py

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,14 @@ def command_info(self, **kwargs) -> None:
743743
def command_count(self, **kwargs) -> ResponseT:
744744
return self.execute_command("COMMAND COUNT", **kwargs)
745745

746+
def command_getkeysandflags(self, *args: List[str]) -> List[Union[str, List[str]]]:
747+
"""
748+
Returns array of keys from a full Redis command and their usage flags.
749+
750+
For more information see https://redis.io/commands/command-getkeysandflags
751+
"""
752+
return self.execute_command("COMMAND GETKEYSANDFLAGS", *args)
753+
746754
def command_docs(self, *args):
747755
"""
748756
This function throws a NotImplementedError since it is intentionally
@@ -752,20 +760,28 @@ def command_docs(self, *args):
752760
"COMMAND DOCS is intentionally not implemented in the client."
753761
)
754762

755-
def config_get(self, pattern: PatternT = "*", **kwargs) -> ResponseT:
763+
def config_get(
764+
self, pattern: PatternT = "*", *args: List[PatternT], **kwargs
765+
) -> ResponseT:
756766
"""
757767
Return a dictionary of configuration based on the ``pattern``
758768
759769
For more information see https://redis.io/commands/config-get
760770
"""
761-
return self.execute_command("CONFIG GET", pattern, **kwargs)
771+
return self.execute_command("CONFIG GET", pattern, *args, **kwargs)
762772

763-
def config_set(self, name: KeyT, value: EncodableT, **kwargs) -> ResponseT:
773+
def config_set(
774+
self,
775+
name: KeyT,
776+
value: EncodableT,
777+
*args: List[Union[KeyT, EncodableT]],
778+
**kwargs,
779+
) -> ResponseT:
764780
"""Set config item ``name`` with ``value``
765781
766782
For more information see https://redis.io/commands/config-set
767783
"""
768-
return self.execute_command("CONFIG SET", name, value, **kwargs)
784+
return self.execute_command("CONFIG SET", name, value, *args, **kwargs)
769785

770786
def config_resetstat(self, **kwargs) -> ResponseT:
771787
"""
@@ -884,7 +900,9 @@ def select(self, index: int, **kwargs) -> ResponseT:
884900
"""
885901
return self.execute_command("SELECT", index, **kwargs)
886902

887-
def info(self, section: Union[str, None] = None, **kwargs) -> ResponseT:
903+
def info(
904+
self, section: Union[str, None] = None, *args: List[str], **kwargs
905+
) -> ResponseT:
888906
"""
889907
Returns a dictionary containing information about the Redis server
890908
@@ -899,7 +917,7 @@ def info(self, section: Union[str, None] = None, **kwargs) -> ResponseT:
899917
if section is None:
900918
return self.execute_command("INFO", **kwargs)
901919
else:
902-
return self.execute_command("INFO", section, **kwargs)
920+
return self.execute_command("INFO", section, *args, **kwargs)
903921

904922
def lastsave(self, **kwargs) -> ResponseT:
905923
"""
@@ -5574,6 +5592,27 @@ def module_load(self, path, *args) -> ResponseT:
55745592
"""
55755593
return self.execute_command("MODULE LOAD", path, *args)
55765594

5595+
def module_loadex(
5596+
self,
5597+
path: str,
5598+
options: Optional[List[str]] = None,
5599+
args: Optional[List[str]] = None,
5600+
) -> ResponseT:
5601+
"""
5602+
Loads a module from a dynamic library at runtime with configuration directives.
5603+
5604+
For more information see https://redis.io/commands/module-loadex
5605+
"""
5606+
pieces = []
5607+
if options is not None:
5608+
pieces.append("CONFIG")
5609+
pieces.extend(options)
5610+
if args is not None:
5611+
pieces.append("ARGS")
5612+
pieces.extend(args)
5613+
5614+
return self.execute_command("MODULE LOADEX", path, *pieces)
5615+
55775616
def module_unload(self, name) -> ResponseT:
55785617
"""
55795618
Unloads the module ``name``.
@@ -5703,28 +5742,20 @@ class FunctionCommands:
57035742

57045743
def function_load(
57055744
self,
5706-
engine: str,
5707-
library: str,
57085745
code: str,
57095746
replace: Optional[bool] = False,
5710-
description: Optional[str] = None,
57115747
) -> Union[Awaitable[str], str]:
57125748
"""
57135749
Load a library to Redis.
5714-
:param engine: the name of the execution engine for the library
5715-
:param library: the unique name of the library
5716-
:param code: the source code
5717-
:param replace: changes the behavior to replace the library if a library called
5718-
``library`` already exists
5719-
:param description: description to the library
5750+
:param code: the source code (must start with
5751+
Shebang statement that provides a metadata about the library)
5752+
:param replace: changes the behavior to overwrite the existing library
5753+
with the new contents.
5754+
Return the library name that was loaded.
57205755
57215756
For more information see https://redis.io/commands/function-load
57225757
"""
5723-
pieces = [engine, library]
5724-
if replace:
5725-
pieces.append("REPLACE")
5726-
if description is not None:
5727-
pieces.append(description)
5758+
pieces = ["REPLACE"] if replace else []
57285759
pieces.append(code)
57295760
return self.execute_command("FUNCTION LOAD", *pieces)
57305761

redis/commands/search/field.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,20 @@ class TagField(Field):
105105
"""
106106

107107
SEPARATOR = "SEPARATOR"
108+
CASESENSITIVE = "CASESENSITIVE"
108109

109-
def __init__(self, name: str, separator: str = ",", **kwargs):
110-
Field.__init__(
111-
self, name, args=[Field.TAG, self.SEPARATOR, separator], **kwargs
112-
)
110+
def __init__(
111+
self,
112+
name: str,
113+
separator: str = ",",
114+
case_sensitive: bool = False,
115+
**kwargs,
116+
):
117+
args = [Field.TAG, self.SEPARATOR, separator]
118+
if case_sensitive:
119+
args.append(self.CASESENSITIVE)
120+
121+
Field.__init__(self, name, args=args, **kwargs)
113122

114123

115124
class VectorField(Field):

tests/test_commands.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,12 @@ def test_config_get(self, r):
669669
# # assert 'maxmemory' in data
670670
# assert data['maxmemory'].isdigit()
671671

672+
@skip_if_server_version_lt("7.0.0")
673+
def test_config_get_multi_params(self, r: redis.Redis):
674+
res = r.config_get("*max-*-entries*", "maxmemory")
675+
assert "maxmemory" in res
676+
assert "hash-max-listpack-entries" in res
677+
672678
@pytest.mark.onlynoncluster
673679
@skip_if_redis_enterprise()
674680
def test_config_resetstat(self, r):
@@ -686,6 +692,16 @@ def test_config_set(self, r):
686692
assert r.config_set("timeout", 0)
687693
assert r.config_get()["timeout"] == "0"
688694

695+
@skip_if_server_version_lt("7.0.0")
696+
@skip_if_redis_enterprise()
697+
def test_config_set_multi_params(self, r: redis.Redis):
698+
r.config_set("timeout", 70, "maxmemory", 100)
699+
assert r.config_get()["timeout"] == "70"
700+
assert r.config_get()["maxmemory"] == "100"
701+
assert r.config_set("timeout", 0, "maxmemory", 0)
702+
assert r.config_get()["timeout"] == "0"
703+
assert r.config_get()["maxmemory"] == "0"
704+
689705
@skip_if_server_version_lt("6.0.0")
690706
@skip_if_redis_enterprise()
691707
def test_failover(self, r):
@@ -711,6 +727,14 @@ def test_info(self, r):
711727
assert "arch_bits" in info.keys()
712728
assert "redis_version" in info.keys()
713729

730+
@pytest.mark.onlynoncluster
731+
@skip_if_server_version_lt("7.0.0")
732+
def test_info_multi_sections(self, r):
733+
res = r.info("clients", "server")
734+
assert isinstance(res, dict)
735+
assert "redis_version" in res
736+
assert "connected_clients" in res
737+
714738
@pytest.mark.onlynoncluster
715739
@skip_if_redis_enterprise()
716740
def test_lastsave(self, r):
@@ -4468,6 +4492,15 @@ def test_command(self, r):
44684492
assert "set" in cmds
44694493
assert "get" in cmds
44704494

4495+
@pytest.mark.onlynoncluster
4496+
@skip_if_server_version_lt("7.0.0")
4497+
@skip_if_redis_enterprise()
4498+
def test_command_getkeysandflags(self, r: redis.Redis):
4499+
res = [["mylist1", ["RW", "access", "delete"]], ["mylist2", ["RW", "insert"]]]
4500+
assert res == r.command_getkeysandflags(
4501+
"LMOVE", "mylist1", "mylist2", "left", "left"
4502+
)
4503+
44714504
@pytest.mark.onlynoncluster
44724505
@skip_if_server_version_lt("4.0.0")
44734506
@skip_if_redis_enterprise()
@@ -4480,6 +4513,18 @@ def test_module(self, r):
44804513
r.module_load("/some/fake/path", "arg1", "arg2", "arg3", "arg4")
44814514
assert "Error loading the extension." in str(excinfo.value)
44824515

4516+
@pytest.mark.onlynoncluster
4517+
@skip_if_server_version_lt("7.0.0")
4518+
@skip_if_redis_enterprise()
4519+
def test_module_loadex(self, r: redis.Redis):
4520+
with pytest.raises(redis.exceptions.ModuleError) as excinfo:
4521+
r.module_loadex("/some/fake/path")
4522+
assert "Error loading the extension." in str(excinfo.value)
4523+
4524+
with pytest.raises(redis.exceptions.ModuleError) as excinfo:
4525+
r.module_loadex("/some/fake/path", ["name", "value"], ["arg1", "arg2"])
4526+
assert "Error loading the extension." in str(excinfo.value)
4527+
44834528
@skip_if_server_version_lt("2.6.0")
44844529
def test_restore(self, r):
44854530

tests/test_function.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
from .conftest import skip_if_server_version_lt
66

7-
function = "redis.register_function('myfunc', function(keys, args) return args[1] end)"
7+
engine = "lua"
8+
lib = "mylib"
9+
lib2 = "mylib2"
10+
function = "redis.register_function{function_name='myfunc', callback=function(keys, \
11+
args) return args[1] end, flags={ 'no-writes' }}"
812
function2 = "redis.register_function('hello', function() return 'Hello World' end)"
9-
set_function = "redis.register_function('set', function(keys, args) \
10-
return redis.call('SET', keys[1], args[1]) end)"
11-
get_function = "redis.register_function('get', function(keys, args) \
12-
return redis.call('GET', keys[1]) end)"
13+
set_function = "redis.register_function('set', function(keys, args) return \
14+
redis.call('SET', keys[1], args[1]) end)"
15+
get_function = "redis.register_function('get', function(keys, args) return \
16+
redis.call('GET', keys[1]) end)"
1317

1418

1519
@skip_if_server_version_lt("7.0.0")
@@ -19,25 +23,28 @@ def reset_functions(self, r):
1923
r.function_flush()
2024

2125
def test_function_load(self, r):
22-
assert r.function_load("Lua", "mylib", function)
23-
assert r.function_load("Lua", "mylib", function, replace=True)
26+
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
27+
assert lib == r.function_load(f"#!{engine} name={lib} \n {function}")
28+
assert lib == r.function_load(
29+
f"#!{engine} name={lib} \n {function}", replace=True
30+
)
2431
with pytest.raises(ResponseError):
25-
r.function_load("Lua", "mylib", function)
32+
r.function_load(f"#!{engine} name={lib} \n {function}")
2633
with pytest.raises(ResponseError):
27-
r.function_load("Lua", "mylib2", function)
34+
r.function_load(f"#!{engine} name={lib2} \n {function}")
2835

2936
def test_function_delete(self, r):
30-
r.function_load("Lua", "mylib", set_function)
37+
r.function_load(f"#!{engine} name={lib} \n {set_function}")
3138
with pytest.raises(ResponseError):
32-
r.function_load("Lua", "mylib", set_function)
39+
r.function_load(f"#!{engine} name={lib} \n {set_function}")
3340
assert r.fcall("set", 1, "foo", "bar") == "OK"
3441
assert r.function_delete("mylib")
3542
with pytest.raises(ResponseError):
3643
r.fcall("set", 1, "foo", "bar")
37-
assert r.function_load("Lua", "mylib", set_function)
44+
assert lib == r.function_load(f"#!{engine} name={lib} \n {set_function}")
3845

3946
def test_function_flush(self, r):
40-
r.function_load("Lua", "mylib", function)
47+
r.function_load(f"#!{engine} name={lib} \n {function}")
4148
assert r.fcall("myfunc", 0, "hello") == "hello"
4249
assert r.function_flush()
4350
with pytest.raises(ResponseError):
@@ -47,36 +54,35 @@ def test_function_flush(self, r):
4754

4855
@pytest.mark.onlynoncluster
4956
def test_function_list(self, r):
50-
r.function_load("Lua", "mylib", function)
57+
r.function_load(f"#!{engine} name={lib} \n {function}")
5158
res = [
5259
[
5360
"library_name",
5461
"mylib",
5562
"engine",
5663
"LUA",
57-
"description",
58-
None,
5964
"functions",
60-
[["name", "myfunc", "description", None]],
65+
[["name", "myfunc", "description", None, "flags", ["no-writes"]]],
6166
],
6267
]
6368
assert r.function_list() == res
6469
assert r.function_list(library="*lib") == res
65-
assert r.function_list(withcode=True)[0][9] == function
70+
assert (
71+
r.function_list(withcode=True)[0][7]
72+
== f"#!{engine} name={lib} \n {function}"
73+
)
6674

6775
@pytest.mark.onlycluster
6876
def test_function_list_on_cluster(self, r):
69-
r.function_load("Lua", "mylib", function)
77+
r.function_load(f"#!{engine} name={lib} \n {function}")
7078
function_list = [
7179
[
7280
"library_name",
7381
"mylib",
7482
"engine",
7583
"LUA",
76-
"description",
77-
None,
7884
"functions",
79-
[["name", "myfunc", "description", None]],
85+
[["name", "myfunc", "description", None, "flags", ["no-writes"]]],
8086
],
8187
]
8288
primaries = r.get_primaries()
@@ -86,33 +92,36 @@ def test_function_list_on_cluster(self, r):
8692
assert r.function_list() == res
8793
assert r.function_list(library="*lib") == res
8894
node = primaries[0].name
89-
assert r.function_list(withcode=True)[node][0][9] == function
95+
assert (
96+
r.function_list(withcode=True)[node][0][7]
97+
== f"#!{engine} name={lib} \n {function}"
98+
)
9099

91100
def test_fcall(self, r):
92-
r.function_load("Lua", "mylib", set_function)
93-
r.function_load("Lua", "mylib2", get_function)
101+
r.function_load(f"#!{engine} name={lib} \n {set_function}")
102+
r.function_load(f"#!{engine} name={lib2} \n {get_function}")
94103
assert r.fcall("set", 1, "foo", "bar") == "OK"
95104
assert r.fcall("get", 1, "foo") == "bar"
96105
with pytest.raises(ResponseError):
97106
r.fcall("myfunc", 0, "hello")
98107

99108
def test_fcall_ro(self, r):
100-
r.function_load("Lua", "mylib", function)
109+
r.function_load(f"#!{engine} name={lib} \n {function}")
101110
assert r.fcall_ro("myfunc", 0, "hello") == "hello"
102-
r.function_load("Lua", "mylib2", set_function)
111+
r.function_load(f"#!{engine} name={lib2} \n {set_function}")
103112
with pytest.raises(ResponseError):
104113
r.fcall_ro("set", 1, "foo", "bar")
105114

106115
def test_function_dump_restore(self, r):
107-
r.function_load("Lua", "mylib", set_function)
116+
r.function_load(f"#!{engine} name={lib} \n {set_function}")
108117
payload = r.function_dump()
109118
assert r.fcall("set", 1, "foo", "bar") == "OK"
110119
r.function_delete("mylib")
111120
with pytest.raises(ResponseError):
112121
r.fcall("set", 1, "foo", "bar")
113122
assert r.function_restore(payload)
114123
assert r.fcall("set", 1, "foo", "bar") == "OK"
115-
r.function_load("Lua", "mylib2", get_function)
124+
r.function_load(f"#!{engine} name={lib2} \n {get_function}")
116125
assert r.fcall("get", 1, "foo") == "bar"
117126
r.function_delete("mylib")
118127
assert r.function_restore(payload, "FLUSH")

0 commit comments

Comments
 (0)