Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implemented COMMAND DOCS by always throwing NotImplementedError #2020

Merged
merged 8 commits into from
Mar 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

* Add `items` parameter to `hset` signature
* Create codeql-analysis.yml (#1988). Thanks @chayim
* Add limited support for Lua scripting with RedisCluster
* Implement `.lock()` method on RedisCluster

* 4.1.3 (Feb 8, 2022)
* Fix flushdb and flushall (#1926)
* Add redis5 and redis4 dockers (#1871)
Expand Down
44 changes: 43 additions & 1 deletion redis/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ class RedisCluster(RedisClusterCommands):
"ACL SETUSER",
"ACL USERS",
"ACL WHOAMI",
"AUTH",
"CLIENT LIST",
"CLIENT SETNAME",
"CLIENT GETNAME",
Expand Down Expand Up @@ -283,19 +284,29 @@ class RedisCluster(RedisClusterCommands):
"READONLY",
"READWRITE",
"TIME",
"GRAPH.CONFIG",
],
DEFAULT_NODE,
),
list_keys_to_dict(
[
"FLUSHALL",
"FLUSHDB",
"FUNCTION DELETE",
"FUNCTION FLUSH",
"FUNCTION LIST",
"FUNCTION LOAD",
"FUNCTION RESTORE",
"SCRIPT EXISTS",
"SCRIPT FLUSH",
"SCRIPT LOAD",
],
PRIMARIES,
),
list_keys_to_dict(
["FUNCTION DUMP"],
RANDOM,
),
list_keys_to_dict(
[
"CLUSTER COUNTKEYSINSLOT",
Expand Down Expand Up @@ -809,6 +820,10 @@ def lock(
thread_local=thread_local,
)

def set_response_callback(self, command, callback):
"""Set a custom Response Callback"""
self.cluster_response_callbacks[command] = callback

def _determine_nodes(self, *args, **kwargs):
command = args[0]
nodes_flag = kwargs.pop("nodes_flag", None)
Expand Down Expand Up @@ -910,6 +925,10 @@ def determine_slot(self, *args):
else:
keys = self._get_command_keys(*args)
if keys is None or len(keys) == 0:
# FCALL can call a function with 0 keys, that means the function
# can be run on any node so we can just return a random slot
if command in ("FCALL", "FCALL_RO"):
return random.randrange(0, REDIS_CLUSTER_HASH_SLOTS)
raise RedisClusterException(
"No way to dispatch this command to Redis Cluster. "
"Missing key.\nYou can execute the command by specifying "
Expand Down Expand Up @@ -1180,6 +1199,20 @@ def _process_result(self, command, res, **kwargs):
else:
return res

def load_external_module(
self,
funcname,
func,
):
"""
This function can be used to add externally defined redis modules,
and their namespaces to the redis client.

``funcname`` - A string containing the name of the function to create
``func`` - The function, being added to this class.
"""
setattr(self, funcname, func)


class ClusterNode:
def __init__(self, host, port, server_type=None, redis_connection=None):
Expand Down Expand Up @@ -2025,7 +2058,13 @@ def _send_cluster_commands(

# turn the response back into a simple flat array that corresponds
# to the sequence of commands issued in the stack in pipeline.execute()
response = [c.result for c in sorted(stack, key=lambda x: x.position)]
response = []
for c in sorted(stack, key=lambda x: x.position):
if c.args[0] in self.cluster_response_callbacks:
c.result = self.cluster_response_callbacks[c.args[0]](
c.result, **c.options
)
response.append(c.result)

if raise_on_error:
self.raise_first_error(stack)
Expand All @@ -2039,6 +2078,9 @@ def _fail_on_redirect(self, allow_redirections):
"ASK & MOVED redirection not allowed in this pipeline"
)

def exists(self, *keys):
return self.execute_command("EXISTS", *keys)

def eval(self):
""" """
raise RedisClusterException("method eval() is not implemented")
Expand Down
4 changes: 4 additions & 0 deletions redis/commands/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from .core import (
ACLCommands,
DataAccessCommands,
FunctionCommands,
ManagementCommands,
PubSubCommands,
ScriptCommands,
)
from .helpers import list_or_args
from .redismodules import RedisModuleCommands


class ClusterMultiKeyCommands:
Expand Down Expand Up @@ -212,6 +214,8 @@ class RedisClusterCommands(
PubSubCommands,
ClusterDataAccessCommands,
ScriptCommands,
FunctionCommands,
RedisModuleCommands,
):
"""
A class for all Redis Cluster commands
Expand Down
30 changes: 22 additions & 8 deletions redis/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,14 +369,16 @@ class ManagementCommands(CommandsProtocol):
Redis management commands
"""

def auth(self):
def auth(self, password, username=None, **kwargs):
"""
This function throws a NotImplementedError since it is intentionally
not supported.
Authenticates the user. If you do not pass username, Redis will try to
authenticate for the "default" user. If you do pass username, it will
authenticate for the given user.
For more information check https://redis.io/commands/auth
"""
raise NotImplementedError(
"AUTH is intentionally not implemented in the client."
)
if username:
return self.execute_command("AUTH", username, password, **kwargs)
return self.execute_command

def bgrewriteaof(self, **kwargs):
"""Tell the Redis server to rewrite the AOF file from data in memory.
Expand Down Expand Up @@ -741,6 +743,15 @@ def command_info(self, **kwargs) -> None:
def command_count(self, **kwargs) -> ResponseT:
return self.execute_command("COMMAND COUNT", **kwargs)

def command_docs(self, *args):
"""
This function throws a NotImplementedError since it is intentionally
not supported.
"""
raise NotImplementedError(
"COMMAND DOCS is intentionally not implemented in the client."
)

def config_get(self, pattern: PatternT = "*", **kwargs) -> ResponseT:
"""
Return a dictionary of configuration based on the ``pattern``
Expand Down Expand Up @@ -4587,18 +4598,21 @@ def hset(
key: Optional[str] = None,
value: Optional[str] = None,
mapping: Optional[dict] = None,
items: Optional[list] = None,
) -> Union[Awaitable[int], int]:
"""
Set ``key`` to ``value`` within hash ``name``,
``mapping`` accepts a dict of key/value pairs that will be
added to hash ``name``.
``items`` accepts a list of key/value pairs that will be
added to hash ``name``.
Returns the number of fields that were added.

For more information check https://redis.io/commands/hset
"""
if key is None and not mapping:
if key is None and not mapping and not items:
raise DataError("'hset' with no key value pairs")
items = []
items = items or []
if key is not None:
items.extend((key, value))
if mapping:
Expand Down
30 changes: 24 additions & 6 deletions redis/commands/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,34 @@ def pipeline(self, transaction=True, shard_hint=None):
pipe.jsonget('foo')
pipe.jsonget('notakey')
"""
p = Pipeline(
connection_pool=self.client.connection_pool,
response_callbacks=self.MODULE_CALLBACKS,
transaction=transaction,
shard_hint=shard_hint,
)
if isinstance(self.client, redis.RedisCluster):
p = ClusterPipeline(
nodes_manager=self.client.nodes_manager,
commands_parser=self.client.commands_parser,
startup_nodes=self.client.nodes_manager.startup_nodes,
result_callbacks=self.client.result_callbacks,
cluster_response_callbacks=self.client.cluster_response_callbacks,
cluster_error_retry_attempts=self.client.cluster_error_retry_attempts,
read_from_replicas=self.client.read_from_replicas,
reinitialize_steps=self.client.reinitialize_steps,
)

else:
p = Pipeline(
connection_pool=self.client.connection_pool,
response_callbacks=self.MODULE_CALLBACKS,
transaction=transaction,
shard_hint=shard_hint,
)

p._encode = self._encode
p._decode = self._decode
return p


class ClusterPipeline(JSONCommands, redis.cluster.ClusterPipeline):
"""Cluster pipeline for the module."""


class Pipeline(JSONCommands, redis.client.Pipeline):
"""Pipeline for the module."""
9 changes: 8 additions & 1 deletion redis/commands/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ def __init__(self, redis_connection):
self.initialize(redis_connection)

def initialize(self, r):
self.commands = r.execute_command("COMMAND")
commands = r.execute_command("COMMAND")
uppercase_commands = []
for cmd in commands:
if any(x.isupper() for x in cmd):
uppercase_commands.append(cmd)
for cmd in uppercase_commands:
commands[cmd.lower()] = commands.pop(cmd)
self.commands = commands

# As soon as this PR is merged into Redis, we should reimplement
# our logic to use COMMAND INFO changes to determine the key positions
Expand Down
31 changes: 24 additions & 7 deletions redis/commands/timeseries/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import redis.client
import redis

from ..helpers import parse_to_list
from .commands import (
Expand Down Expand Up @@ -67,14 +67,31 @@ def pipeline(self, transaction=True, shard_hint=None):
pipeline.execute()

"""
p = Pipeline(
connection_pool=self.client.connection_pool,
response_callbacks=self.MODULE_CALLBACKS,
transaction=transaction,
shard_hint=shard_hint,
)
if isinstance(self.client, redis.RedisCluster):
p = ClusterPipeline(
nodes_manager=self.client.nodes_manager,
commands_parser=self.client.commands_parser,
startup_nodes=self.client.nodes_manager.startup_nodes,
result_callbacks=self.client.result_callbacks,
cluster_response_callbacks=self.client.cluster_response_callbacks,
cluster_error_retry_attempts=self.client.cluster_error_retry_attempts,
read_from_replicas=self.client.read_from_replicas,
reinitialize_steps=self.client.reinitialize_steps,
)

else:
p = Pipeline(
connection_pool=self.client.connection_pool,
response_callbacks=self.MODULE_CALLBACKS,
transaction=transaction,
shard_hint=shard_hint,
)
return p


class ClusterPipeline(TimeSeriesCommands, redis.cluster.ClusterPipeline):
"""Cluster pipeline for the module."""


class Pipeline(TimeSeriesCommands, redis.client.Pipeline):
"""Pipeline for the module."""
1 change: 1 addition & 0 deletions tests/test_bloom.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ def test_cms(client):


@pytest.mark.redismod
@pytest.mark.onlynoncluster
def test_cms_merge(client):
assert client.cms().initbydim("A", 1000, 5)
assert client.cms().initbydim("B", 1000, 5)
Expand Down
40 changes: 36 additions & 4 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,32 @@ def test_case_insensitive_command_names(self, r):


class TestRedisCommands:
def test_auth(self, r, request):
username = "redis-py-auth"

def teardown():
r.acl_deluser(username)

request.addfinalizer(teardown)

assert r.acl_setuser(
username,
enabled=True,
passwords=["+strong_password"],
commands=["+acl"],
)

assert r.auth(username=username, password="strong_password") is True

with pytest.raises(exceptions.ResponseError):
r.auth(username=username, password="wrong_password")

def test_command_on_invalid_key_type(self, r):
r.lpush("a", "1")
with pytest.raises(redis.ResponseError):
r["a"]

# SERVER INFORMATION
def test_auth_not_implemented(self, r):
with pytest.raises(NotImplementedError):
r.auth()

@skip_if_server_version_lt("6.0.0")
def test_acl_cat_no_category(self, r):
categories = r.acl_cat()
Expand Down Expand Up @@ -2530,6 +2546,17 @@ def test_hset_with_multi_key_values(self, r):
assert r.hget("b", "2") == b"2"
assert r.hget("b", "foo") == b"bar"

def test_hset_with_key_values_passed_as_list(self, r):
r.hset("a", items=["1", 1, "2", 2, "3", 3])
assert r.hget("a", "1") == b"1"
assert r.hget("a", "2") == b"2"
assert r.hget("a", "3") == b"3"

r.hset("b", "foo", "bar", items=["1", 1, "2", 2])
assert r.hget("b", "1") == b"1"
assert r.hget("b", "2") == b"2"
assert r.hget("b", "foo") == b"bar"

def test_hset_without_data(self, r):
with pytest.raises(exceptions.DataError):
r.hset("x")
Expand Down Expand Up @@ -4212,6 +4239,11 @@ def test_command_count(self, r):
assert isinstance(res, int)
assert res >= 100

@skip_if_server_version_lt("7.0.0")
def test_command_docs(self, r):
with pytest.raises(NotImplementedError):
r.command_docs("set")

@pytest.mark.onlynoncluster
@skip_if_server_version_lt("2.8.13")
def test_command_getkeys(self, r):
Expand Down
Loading