Repository: redis/redis-py

Issue:
The method xgroup_create of the redis cluster client fails when running on redis version 7.0.0.
**Describe the bug**

The method xgroup_create of the redis cluster client fails when running on redis version 7.0.0.
Using the version 4.2.2 of the python redis client, the python version is 3.10.4, OS is Ubuntu 22.04.

**To reproduce**

```

from redis import RedisCluster
from redis.cluster import ClusterNode

client = RedisCluster(
    startup_nodes=[
        ClusterNode("127.0.0.1", 7001),
        ClusterNode("127.0.0.1", 7002),
        ClusterNode("127.0.0.1", 7003),
        ClusterNode("127.0.0.1", 7004),
        ClusterNode("127.0.0.1", 7005),
    ]
)

client.xgroup_create("test", "test")
```

**Expected behavior**

The method should work just like it did when using the same client version with redis version 6.2.6.

**Additional information**

This is the output of the python stack trace.

```
File ~/.cache/pypoetry/virtualenvs/poetry_venv-py3.10/lib/python3.10/site-packages/redis/cluster.py:976, in RedisCluster.determine_slot(self, *args)File ~/.cache/pypoetry/virtualenvs/ubi.events-engine-94LnMkf1-py3.10/lib/python3.10/site-packages/redis/cluster.py:976, in RedisCluster.determine_slot(self, *args)
    974         if command in ("FCALL", "FCALL_RO"):
    975             return random.randrange(0, REDIS_CLUSTER_HASH_SLOTS)
--> 976         raise RedisClusterException(
    977             "No way to dispatch this command to Redis Cluster. "
    978             "Missing key.\nYou can execute the command by specifying "
    979             f"target nodes.\nCommand: {args}"
    980         )
    982 # single key command
    983 if len(keys) == 1:

RedisClusterException: No way to dispatch this command to Redis Cluster. Missing key.
You can execute the command by specifying target nodes.
Command: ('XGROUP CREATE', 'test', 'test', '$')
```




Discussion:

Resolving patch:
diff --git a/redis/client.py b/redis/client.py
--- a/redis/client.py
+++ b/redis/client.py
@@ -557,6 +557,10 @@ def parse_command(response, **options):
         cmd_dict["first_key_pos"] = command[3]
         cmd_dict["last_key_pos"] = command[4]
         cmd_dict["step_count"] = command[5]
+        if len(command) > 7:
+            cmd_dict["tips"] = command[7]
+            cmd_dict["key_specifications"] = command[8]
+            cmd_dict["subcommands"] = command[9]
         commands[cmd_name] = cmd_dict
     return commands
 
diff --git a/redis/cluster.py b/redis/cluster.py
--- a/redis/cluster.py
+++ b/redis/cluster.py
@@ -223,6 +223,7 @@ class RedisCluster(RedisClusterCommands):
             [
                 "ACL CAT",
                 "ACL DELUSER",
+                "ACL DRYRUN",
                 "ACL GENPASS",
                 "ACL GETUSER",
                 "ACL HELP",
diff --git a/redis/commands/parser.py b/redis/commands/parser.py
--- a/redis/commands/parser.py
+++ b/redis/commands/parser.py
@@ -26,6 +26,21 @@ def initialize(self, r):
             commands[cmd.lower()] = commands.pop(cmd)
         self.commands = commands
 
+    def parse_subcommand(self, command, **options):
+        cmd_dict = {}
+        cmd_name = str_if_bytes(command[0])
+        cmd_dict["name"] = cmd_name
+        cmd_dict["arity"] = int(command[1])
+        cmd_dict["flags"] = [str_if_bytes(flag) for flag in command[2]]
+        cmd_dict["first_key_pos"] = command[3]
+        cmd_dict["last_key_pos"] = command[4]
+        cmd_dict["step_count"] = command[5]
+        if len(command) > 7:
+            cmd_dict["tips"] = command[7]
+            cmd_dict["key_specifications"] = command[8]
+            cmd_dict["subcommands"] = command[9]
+        return cmd_dict
+
     # As soon as this PR is merged into Redis, we should reimplement
     # our logic to use COMMAND INFO changes to determine the key positions
     # https://github.com/redis/redis/pull/8324
@@ -73,8 +88,17 @@ def get_keys(self, redis_conn, *args):
                 and command["first_key_pos"] == 0
                 and command["last_key_pos"] == 0
             ):
+                is_subcmd = False
+                if "subcommands" in command:
+                    subcmd_name = f"{cmd_name}|{args[1].lower()}"
+                    for subcmd in command["subcommands"]:
+                        if str_if_bytes(subcmd[0]) == subcmd_name:
+                            command = self.parse_subcommand(subcmd)
+                            is_subcmd = True
+
                 # The command doesn't have keys in it
-                return None
+                if not is_subcmd:
+                    return None
             last_key_pos = command["last_key_pos"]
             if last_key_pos < 0:
                 last_key_pos = len(args) - abs(last_key_pos)


Test patch:
diff --git a/tests/test_cluster.py b/tests/test_cluster.py
--- a/tests/test_cluster.py
+++ b/tests/test_cluster.py
@@ -1098,7 +1098,6 @@ def test_cluster_links(self, r):
         links_to = sum(x.count("to") for x in res)
         links_for = sum(x.count("from") for x in res)
         assert links_to == links_for
-        print(res)
         for i in range(0, len(res) - 1, 2):
             assert res[i][3] == res[i + 1][3]
 
diff --git a/tests/test_commands.py b/tests/test_commands.py
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -1164,7 +1164,7 @@ def test_expire_option_xx(self, r):
         r.set("key", "val")
         assert r.expire("key", 100, xx=True) == 0
         assert r.expire("key", 100)
-        assert r.expire("key", 500, nx=True) == 1
+        assert r.expire("key", 500, xx=True) == 1
 
     @skip_if_server_version_lt("7.0.0")
     def test_expire_option_gt(self, r):
@@ -3012,6 +3012,7 @@ def test_sort_all_options(self, r):
         assert r.lrange("sorted", 0, 10) == [b"vodka", b"milk", b"gin", b"apple juice"]
 
     @skip_if_server_version_lt("7.0.0")
+    @pytest.mark.onlynoncluster
     def test_sort_ro(self, r):
         r["score:1"] = 8
         r["score:2"] = 3