Repository: redis/redis-py
Pull request #1703 (2021-11-14 10:31:47)

Unit tests fixes for compatibility
- Fixing multiprocessing tests to respect redisurl ports
- Test improvement: do not fill modules information if no modules present
- Marking module loader as redismod
- Database override support
- Pubsub test fix
- Enterprise tests support
- Test validation and skips
- Skips for redis enterprise
- Adding support for redis command COMMAND



Patch:
diff --git a/redis/client.py b/redis/client.py
index f2f1eed0..753770e3 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -703,7 +703,6 @@ class Redis(RedisModuleCommands, CoreCommands, object):
         'CLUSTER SET-CONFIG-EPOCH': bool_ok,
         'CLUSTER SETSLOT': bool_ok,
         'CLUSTER SLAVES': parse_cluster_nodes,
-        'COMMAND': int,
         'COMMAND COUNT': int,
         'CONFIG GET': parse_config_get,
         'CONFIG RESETSTAT': bool_ok,
@@ -891,6 +890,12 @@ class Redis(RedisModuleCommands, CoreCommands, object):
         self.response_callbacks = CaseInsensitiveDict(
             self.__class__.RESPONSE_CALLBACKS)
 
+        # preload our class with the available redis commands
+        try:
+            self.__redis_commands__()
+        except RedisError:
+            pass
+
     def __repr__(self):
         return "%s<%s>" % (type(self).__name__, repr(self.connection_pool))
 
@@ -898,12 +903,12 @@ class Redis(RedisModuleCommands, CoreCommands, object):
         "Set a custom Response Callback"
         self.response_callbacks[command] = callback
 
-    def load_external_module(self, modname, funcname, func):
+    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.
-        modname - A string containing the name of the redis module to look for
-                  in the redis info block.
+
         funcname - A string containing the name of the function to create
         func - The function, being added to this class.
 
@@ -914,31 +919,25 @@ class Redis(RedisModuleCommands, CoreCommands, object):
         from redis import Redis
         from foomodule import F
         r = Redis()
-        r.load_external_module("foomod", "foo", F)
+        r.load_external_module("foo", F)
         r.foo().dothing('your', 'arguments')
 
         For a concrete example see the reimport of the redisjson module in
         tests/test_connection.py::test_loading_external_modules
         """
-        mods = self.loaded_modules
-        if modname.lower() not in mods:
-            raise ModuleError("{} is not loaded in redis.".format(modname))
         setattr(self, funcname, func)
 
-    @property
-    def loaded_modules(self):
-        key = '__redis_modules__'
-        mods = getattr(self, key, None)
-        if mods is not None:
-            return mods
-
+    def __redis_commands__(self):
+        """Store the list of available commands, for our redis instance."""
+        cmds = getattr(self, '__commands__', None)
+        if cmds is not None:
+            return cmds
         try:
-            mods = {f.get('name').lower(): f.get('ver')
-                    for f in self.info().get('modules')}
-        except TypeError:
-            mods = []
-        setattr(self, key, mods)
-        return mods
+            cmds = [c[0].upper().decode() for c in self.command()]
+        except AttributeError:  # if encoded
+            cmds = [c[0].upper() for c in self.command()]
+        self.__commands__ = cmds
+        return cmds
 
     def pipeline(self, transaction=True, shard_hint=None):
         """
diff --git a/redis/commands/core.py b/redis/commands/core.py
index 67f1bfa1..516e7d9c 100644
--- a/redis/commands/core.py
+++ b/redis/commands/core.py
@@ -3315,6 +3315,9 @@ class CoreCommands:
     def command_count(self):
         return self.execute_command('COMMAND COUNT')
 
+    def command(self):
+        return self.execute_command('COMMAND')
+
 
 class Script:
     "An executable Lua script object returned by ``register_script``"
diff --git a/redis/commands/redismodules.py b/redis/commands/redismodules.py
index 457a69e2..b3cbee1d 100644
--- a/redis/commands/redismodules.py
+++ b/redis/commands/redismodules.py
@@ -8,41 +8,41 @@ class RedisModuleCommands:
     """
 
     def json(self, encoder=JSONEncoder(), decoder=JSONDecoder()):
-        """Access the json namespace, providing support for redis json."""
-        try:
-            modversion = self.loaded_modules['rejson']
-        except IndexError:
-            raise ModuleError("rejson is not a loaded in the redis instance.")
+        """Access the json namespace, providing support for redis json.
+        """
+        if 'JSON.SET' not in self.__commands__:
+            raise ModuleError("redisjson is not loaded in redis. "
+                              "For more information visit "
+                              "https://redisjson.io/")
 
         from .json import JSON
         jj = JSON(
                 client=self,
-                version=modversion,
                 encoder=encoder,
                 decoder=decoder)
         return jj
 
     def ft(self, index_name="idx"):
-        """Access the search namespace, providing support for redis search."""
-        try:
-            modversion = self.loaded_modules['search']
-        except IndexError:
-            raise ModuleError("search is not a loaded in the redis instance.")
+        """Access the search namespace, providing support for redis search.
+        """
+        if 'FT.INFO' not in self.__commands__:
+            raise ModuleError("redisearch is not loaded in redis. "
+                              "For more information visit "
+                              "https://redisearch.io/")
 
         from .search import Search
-        s = Search(client=self, version=modversion, index_name=index_name)
+        s = Search(client=self, index_name=index_name)
         return s
 
-    def ts(self, index_name="idx"):
+    def ts(self):
         """Access the timeseries namespace, providing support for
         redis timeseries data.
         """
-        try:
-            modversion = self.loaded_modules['timeseries']
-        except IndexError:
-            raise ModuleError("timeseries is not a loaded in "
-                              "the redis instance.")
+        if 'TS.INFO' not in self.__commands__:
+            raise ModuleError("reditimeseries is not loaded in redis. "
+                              "For more information visit "
+                              "https://redistimeseries.io/")
 
         from .timeseries import TimeSeries
-        s = TimeSeries(client=self, version=modversion, index_name=index_name)
+        s = TimeSeries(client=self)
         return s
diff --git a/redis/commands/search/__init__.py b/redis/commands/search/__init__.py
index 425578ea..8320ad43 100644
--- a/redis/commands/search/__init__.py
+++ b/redis/commands/search/__init__.py
@@ -83,7 +83,7 @@ class Search(SearchCommands):
             self.pipeline.execute()
             self.current_chunk = 0
 
-    def __init__(self, client, version=None, index_name="idx"):
+    def __init__(self, client, index_name="idx"):
         """
         Create a new Client for the given index_name.
         The default name is `idx`
@@ -91,7 +91,6 @@ class Search(SearchCommands):
         If conn is not None, we employ an already existing redis connection
         """
         self.client = client
-        self.MODULE_VERSION = version
         self.index_name = index_name
         self.execute_command = client.execute_command
         self.pipeline = client.pipeline
diff --git a/redis/commands/timeseries/__init__.py b/redis/commands/timeseries/__init__.py
index 83fa1708..5ce538f6 100644
--- a/redis/commands/timeseries/__init__.py
+++ b/redis/commands/timeseries/__init__.py
@@ -34,7 +34,7 @@ class TimeSeries(TimeSeriesCommands):
     functionality.
     """
 
-    def __init__(self, client=None, version=None, **kwargs):
+    def __init__(self, client=None, **kwargs):
         """Create a new RedisTimeSeries client."""
         # Set the module commands' callbacks
         self.MODULE_CALLBACKS = {
@@ -55,7 +55,6 @@ class TimeSeries(TimeSeriesCommands):
 
         self.client = client
         self.execute_command = client.execute_command
-        self.MODULE_VERSION = version
 
         for key, value in self.MODULE_CALLBACKS.items():
             self.client.set_response_callback(key, value)
diff --git a/redis/lock.py b/redis/lock.py
index 326dbaf9..d2297526 100644
--- a/redis/lock.py
+++ b/redis/lock.py
@@ -76,14 +76,14 @@ class Lock:
         Create a new Lock instance named ``name`` using the Redis client
         supplied by ``redis``.
 
-        ``timeout`` indicates a maximum life for the lock.
+        ``timeout`` indicates a maximum life for the lock in seconds.
         By default, it will remain locked until release() is called.
         ``timeout`` can be specified as a float or integer, both representing
         the number of seconds to wait.
 
-        ``sleep`` indicates the amount of time to sleep per loop iteration
-        when the lock is in blocking mode and another client is currently
-        holding the lock.
+        ``sleep`` indicates the amount of time to sleep in seconds per loop
+        iteration when the lock is in blocking mode and another client is
+        currently holding the lock.
 
         ``blocking`` indicates whether calling ``acquire`` should block until
         the lock has been acquired or to fail immediately, causing ``acquire``


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index bb682f78..31d3fbd1 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -31,12 +31,10 @@ def pytest_addoption(parser):
 def _get_info(redis_url):
     client = redis.Redis.from_url(redis_url)
     info = client.info()
-    try:
-        client.execute_command("CONFIG SET maxmemory 5555555")
-        client.execute_command("CONFIG SET maxmemory 0")
-        info["enterprise"] = False
-    except redis.exceptions.ResponseError:
+    if 'dping' in client.__commands__:
         info["enterprise"] = True
+    else:
+        info["enterprise"] = False
     client.connection_pool.disconnect()
     return info
 
@@ -57,6 +55,8 @@ def pytest_sessionstart(session):
         REDIS_INFO["modules"] = info["modules"]
     except redis.exceptions.ConnectionError:
         pass
+    except KeyError:
+        pass
 
 
 def skip_if_server_version_lt(min_version):
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 6cb1a782..dbd04429 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -3671,6 +3671,14 @@ class TestRedisCommands:
         assert isinstance(res, int)
         assert res >= 100
 
+    @skip_if_server_version_lt('2.8.13')
+    def test_command(self, r):
+        res = r.command()
+        assert len(res) >= 100
+        cmds = [c[0].decode() for c in res]
+        assert 'set' in cmds
+        assert 'get' in cmds
+
     @skip_if_server_version_lt('4.0.0')
     @skip_if_redis_enterprise
     def test_module(self, r):
diff --git a/tests/test_connection.py b/tests/test_connection.py
index f2fc8341..7c447681 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -2,7 +2,7 @@ from unittest import mock
 import types
 import pytest
 
-from redis.exceptions import InvalidResponse, ModuleError
+from redis.exceptions import InvalidResponse
 from redis.utils import HIREDIS_AVAILABLE
 from .conftest import skip_if_server_version_lt
 
@@ -19,30 +19,20 @@ def test_invalid_response(r):
 
 @skip_if_server_version_lt('4.0.0')
 @pytest.mark.redismod
-def test_loaded_modules(r, modclient):
-    assert r.loaded_modules == []
-    assert 'rejson' in modclient.loaded_modules.keys()
-
-
-@skip_if_server_version_lt('4.0.0')
-@pytest.mark.redismod
-def test_loading_external_modules(r, modclient):
+def test_loading_external_modules(modclient):
     def inner():
         pass
 
-    with pytest.raises(ModuleError):
-        r.load_external_module('rejson', 'myfuncname', inner)
-
-    modclient.load_external_module('rejson', 'myfuncname', inner)
+    modclient.load_external_module('myfuncname', inner)
     assert getattr(modclient, 'myfuncname') == inner
     assert isinstance(getattr(modclient, 'myfuncname'), types.FunctionType)
 
     # and call it
     from redis.commands import RedisModuleCommands
     j = RedisModuleCommands.json
-    modclient.load_external_module('rejson', 'sometestfuncname', j)
+    modclient.load_external_module('sometestfuncname', j)
 
-    d = {'hello': 'world!'}
-    mod = j(modclient)
-    mod.set("fookey", ".", d)
-    assert mod.get('fookey') == d
+    # d = {'hello': 'world!'}
+    # mod = j(modclient)
+    # mod.set("fookey", ".", d)
+    # assert mod.get('fookey') == d