diff options
-rw-r--r-- | .github/release-drafter-config.yml | 2 | ||||
-rw-r--r-- | .readthedocs.yml | 13 | ||||
-rw-r--r-- | docs/conf.py | 16 | ||||
-rw-r--r-- | docs/index.rst | 16 | ||||
-rw-r--r-- | docs/make.bat | 190 | ||||
-rw-r--r-- | docs/requirements.txt | 2 | ||||
-rwxr-xr-x | redis/client.py | 41 | ||||
-rw-r--r-- | redis/commands/core.py | 10 | ||||
-rw-r--r-- | redis/commands/redismodules.py | 38 | ||||
-rw-r--r-- | redis/commands/search/__init__.py | 3 | ||||
-rw-r--r-- | redis/commands/search/commands.py | 4 | ||||
-rw-r--r-- | redis/commands/timeseries/__init__.py | 3 | ||||
-rw-r--r-- | redis/lock.py | 8 | ||||
-rw-r--r-- | tests/conftest.py | 18 | ||||
-rw-r--r-- | tests/test_commands.py | 91 | ||||
-rw-r--r-- | tests/test_connection.py | 26 | ||||
-rw-r--r-- | tests/test_connection_pool.py | 12 | ||||
-rw-r--r-- | tests/test_monitor.py | 15 | ||||
-rw-r--r-- | tests/test_pubsub.py | 7 | ||||
-rw-r--r-- | tests/test_scripting.py | 11 | ||||
-rw-r--r-- | tests/test_search.py | 12 | ||||
-rw-r--r-- | tox.ini | 3 |
22 files changed, 249 insertions, 292 deletions
diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml index f17a299..a3a5d83 100644 --- a/.github/release-drafter-config.yml +++ b/.github/release-drafter-config.yml @@ -15,7 +15,7 @@ autolabeler: branch: - '/feature-.+' categories: - - title: 'Breaking Changes' + - title: '🔥 Breaking Changes' labels: - 'breakingchange' - title: '🚀 New Features' diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..80b9738 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 + +python: + install: + - requirements: ./docs/requirements.txt + +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +sphinx: + configuration: docs/conf.py diff --git a/docs/conf.py b/docs/conf.py index dfdaf9e..ff37119 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,16 +46,16 @@ master_doc = "index" # General information about the project. project = "redis-py" -copyright = "2016, Andy McCurdy" +copyright = "2021, Redis Inc." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = "2.10.5" +version = "4.0.9" # The full version, including alpha/beta/rc tags. -release = "2.10.5" +release = "4.0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -191,7 +191,7 @@ latex_documents = [ ("index", "redis-py.tex", "redis-py Documentation", - "Andy McCurdy", + "Redis Inc", "manual"), ] @@ -240,7 +240,7 @@ texinfo_documents = [ "index", "redis-py", "redis-py Documentation", - "Andy McCurdy", + "Redis Inc", "redis-py", "One line description of project.", "Miscellaneous", @@ -257,6 +257,6 @@ texinfo_documents = [ # texinfo_show_urls = 'footnote' epub_title = "redis-py" -epub_author = "Andy McCurdy" -epub_publisher = "Andy McCurdy" -epub_copyright = "2011, Andy McCurdy" +epub_author = "Redis Inc" +epub_publisher = "Redis Inc" +epub_copyright = "2021, Redis Inc" diff --git a/docs/index.rst b/docs/index.rst index bc1a4fa..8af5385 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,5 +22,21 @@ Contents: .. automodule:: redis :members: +.. automodule:: redis.backoff + :members: + +.. automodule:: redis.connection + :members: + +.. automodule:: redis.commands + :members: + +.. automodule:: redis.exceptions + :members: + +.. automodule:: redis.lock + :members: + .. automodule:: redis.sentinel :members: + diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index bb2ae4d..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,190 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^<target^>` where ^<target^> is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\redis-py.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\redis-py.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..2e1c4fb --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx<2 +docutils<0.18 diff --git a/redis/client.py b/redis/client.py index f2f1eed..753770e 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 0344e3a..516e7d9 100644 --- a/redis/commands/core.py +++ b/redis/commands/core.py @@ -2520,10 +2520,13 @@ class CoreCommands: ``offset`` and ``num`` are specified, then return a slice of the range. Can't be provided when using ``bylex``. """ - # Supports old implementation: need to support ``desc`` also for version < 6.2.0 - if not byscore and not bylex and (offset is None and num is None) and desc: + # Need to support ``desc`` also when using old redis version + # because it was supported in 3.5.3 (of redis-py) + if not byscore and not bylex and (offset is None and num is None) \ + and desc: return self.zrevrange(name, start, end, withscores, score_cast_func) + return self._zrange('ZRANGE', None, name, start, end, desc, byscore, bylex, withscores, score_cast_func, offset, num) @@ -3312,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 457a69e..b3cbee1 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 425578e..8320ad4 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/search/commands.py b/redis/commands/search/commands.py index 296fb25..0cee2ad 100644 --- a/redis/commands/search/commands.py +++ b/redis/commands/search/commands.py @@ -17,6 +17,7 @@ ADD_CMD = "FT.ADD" ADDHASH_CMD = "FT.ADDHASH" DROP_CMD = "FT.DROP" EXPLAIN_CMD = "FT.EXPLAIN" +EXPLAINCLI_CMD = "FT.EXPLAINCLI" DEL_CMD = "FT.DEL" AGGREGATE_CMD = "FT.AGGREGATE" CURSOR_CMD = "FT.CURSOR" @@ -376,6 +377,9 @@ class SearchCommands: args, query_text = self._mk_query_args(query) return self.execute_command(EXPLAIN_CMD, *args) + def explain_cli(self, query): # noqa + raise NotImplementedError("EXPLAINCLI will not be implemented.") + def aggregate(self, query): """ Issue an aggregation query diff --git a/redis/commands/timeseries/__init__.py b/redis/commands/timeseries/__init__.py index 83fa170..5ce538f 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 326dbaf..d229752 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`` diff --git a/tests/conftest.py b/tests/conftest.py index 0adec91..31d3fbd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,6 +31,10 @@ def pytest_addoption(parser): def _get_info(redis_url): client = redis.Redis.from_url(redis_url) info = client.info() + if 'dping' in client.__commands__: + info["enterprise"] = True + else: + info["enterprise"] = False client.connection_pool.disconnect() return info @@ -42,6 +46,7 @@ def pytest_sessionstart(session): arch_bits = info["arch_bits"] REDIS_INFO["version"] = version REDIS_INFO["arch_bits"] = arch_bits + REDIS_INFO["enterprise"] = info["enterprise"] # module info, if the second redis is running try: @@ -50,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): @@ -92,6 +99,17 @@ def skip_ifmodversion_lt(min_version: str, module_name: str): raise AttributeError("No redis module named {}".format(module_name)) +def skip_if_redis_enterprise(func): + check = REDIS_INFO["enterprise"] is True + return pytest.mark.skipif(check, reason="Redis enterprise" + ) + + +def skip_ifnot_redis_enterprise(func): + check = REDIS_INFO["enterprise"] is False + return pytest.mark.skipif(check, reason="Redis enterprise") + + def _get_client(cls, request, single_connection_client=True, flushdb=True, from_url=None, **kwargs): diff --git a/tests/test_commands.py b/tests/test_commands.py index c361a4b..dbd0442 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -13,6 +13,7 @@ from .conftest import ( _get_client, skip_if_server_version_gte, skip_if_server_version_lt, + skip_if_redis_enterprise, skip_unless_arch_bits, ) @@ -80,6 +81,7 @@ class TestRedisCommands: assert 'get' in commands @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_deluser(self, r, request): username = 'redis-py-user' @@ -104,6 +106,7 @@ class TestRedisCommands: assert r.acl_getuser(users[4]) is None @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_genpass(self, r): password = r.acl_genpass() assert isinstance(password, str) @@ -117,6 +120,7 @@ class TestRedisCommands: assert isinstance(password, str) @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_getuser_setuser(self, r, request): username = 'redis-py-user' @@ -210,6 +214,7 @@ class TestRedisCommands: assert len(res) != 0 @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_list(self, r, request): username = 'redis-py-user' @@ -222,6 +227,7 @@ class TestRedisCommands: assert len(users) == 2 @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_log(self, r, request): username = 'redis-py-user' @@ -257,6 +263,7 @@ class TestRedisCommands: assert r.acl_log_reset() @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_setuser_categories_without_prefix_fails(self, r, request): username = 'redis-py-user' @@ -268,6 +275,7 @@ class TestRedisCommands: r.acl_setuser(username, categories=['list']) @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_setuser_commands_without_prefix_fails(self, r, request): username = 'redis-py-user' @@ -279,6 +287,7 @@ class TestRedisCommands: r.acl_setuser(username, commands=['get']) @skip_if_server_version_lt("6.0.0") + @skip_if_redis_enterprise def test_acl_setuser_add_passwords_and_nopass_fails(self, r, request): username = 'redis-py-user' @@ -312,13 +321,18 @@ class TestRedisCommands: assert 'addr' in info @skip_if_server_version_lt('5.0.0') - def test_client_list_type(self, r): + def test_client_list_types_not_replica(self, r): with pytest.raises(exceptions.RedisError): r.client_list(_type='not a client type') - for client_type in ['normal', 'master', 'replica', 'pubsub']: + for client_type in ['normal', 'master', 'pubsub']: clients = r.client_list(_type=client_type) assert isinstance(clients, list) + @skip_if_redis_enterprise + def test_client_list_replica(self, r): + clients = r.client_list(_type='replica') + assert isinstance(clients, list) + @skip_if_server_version_lt('6.2.0') def test_client_list_client_id(self, r, request): clients = r.client_list() @@ -453,7 +467,8 @@ class TestRedisCommands: client_2_addr = clients_by_name['redis-py-c2'].get('laddr') assert r.client_kill_filter(laddr=client_2_addr) - @skip_if_server_version_lt('2.8.12') + @skip_if_server_version_lt('6.0.0') + @skip_if_redis_enterprise def test_client_kill_filter_by_user(self, r, request): killuser = 'user_to_kill' r.acl_setuser(killuser, enabled=True, reset=True, @@ -467,6 +482,7 @@ class TestRedisCommands: r.acl_deluser(killuser) @skip_if_server_version_lt('2.9.50') + @skip_if_redis_enterprise def test_client_pause(self, r): assert r.client_pause(1) assert r.client_pause(timeout=1) @@ -474,6 +490,7 @@ class TestRedisCommands: r.client_pause(timeout='not an integer') @skip_if_server_version_lt('6.2.0') + @skip_if_redis_enterprise def test_client_unpause(self, r): assert r.client_unpause() == b'OK' @@ -491,15 +508,18 @@ class TestRedisCommands: assert r.get('foo') == b'bar' @skip_if_server_version_lt('6.0.0') + @skip_if_redis_enterprise def test_client_getredir(self, r): assert isinstance(r.client_getredir(), int) assert r.client_getredir() == -1 def test_config_get(self, r): data = r.config_get() - assert 'maxmemory' in data - assert data['maxmemory'].isdigit() + assert len(data.keys()) > 10 + # # assert 'maxmemory' in data + # assert data['maxmemory'].isdigit() + @skip_if_redis_enterprise def test_config_resetstat(self, r): r.ping() prior_commands_processed = int(r.info()['total_commands_processed']) @@ -508,14 +528,12 @@ class TestRedisCommands: reset_commands_processed = int(r.info()['total_commands_processed']) assert reset_commands_processed < prior_commands_processed + @skip_if_redis_enterprise def test_config_set(self, r): - data = r.config_get() - rdbname = data['dbfilename'] - try: - assert r.config_set('dbfilename', 'redis_py_test.rdb') - assert r.config_get()['dbfilename'] == 'redis_py_test.rdb' - finally: - assert r.config_set('dbfilename', rdbname) + r.config_set('timeout', 70) + assert r.config_get()['timeout'] == '70' + assert r.config_set('timeout', 0) + assert r.config_get()['timeout'] == '0' def test_dbsize(self, r): r['a'] = 'foo' @@ -530,8 +548,10 @@ class TestRedisCommands: r['b'] = 'bar' info = r.info() assert isinstance(info, dict) - assert info['db9']['keys'] == 2 + assert 'arch_bits' in info.keys() + assert 'redis_version' in info.keys() + @skip_if_redis_enterprise def test_lastsave(self, r): assert isinstance(r.lastsave(), datetime.datetime) @@ -625,6 +645,7 @@ class TestRedisCommands: assert isinstance(t[0], int) assert isinstance(t[1], int) + @skip_if_redis_enterprise def test_bgsave(self, r): assert r.bgsave() time.sleep(0.3) @@ -1187,6 +1208,12 @@ class TestRedisCommands: value1 = 'ohmytext' value2 = 'mynewtext' res = 'mytext' + + if skip_if_redis_enterprise(None).args[0] is True: + with pytest.raises(redis.exceptions.ResponseError): + assert r.stralgo('LCS', value1, value2) == res + return + # test LCS of strings assert r.stralgo('LCS', value1, value2) == res # test using keys @@ -1229,6 +1256,12 @@ class TestRedisCommands: def test_substr(self, r): r['a'] = '0123456789' + + if skip_if_redis_enterprise(None).args[0] is True: + with pytest.raises(redis.exceptions.ResponseError): + assert r.substr('a', 0) == b'0123456789' + return + assert r.substr('a', 0) == b'0123456789' assert r.substr('a', 2) == b'23456789' assert r.substr('a', 3, 5) == b'345' @@ -2433,6 +2466,7 @@ class TestRedisCommands: 'slaves', 'nodeid'), dict) @skip_if_server_version_lt('3.0.0') + @skip_if_redis_enterprise def test_readwrite(self, r): assert r.readwrite() @@ -3595,6 +3629,11 @@ class TestRedisCommands: @skip_if_server_version_lt('4.0.0') def test_memory_malloc_stats(self, r): + if skip_if_redis_enterprise(None).args[0] is True: + with pytest.raises(redis.exceptions.ResponseError): + assert r.memory_malloc_stats() + return + assert r.memory_malloc_stats() @skip_if_server_version_lt('4.0.0') @@ -3602,6 +3641,12 @@ class TestRedisCommands: # put a key into the current db to make sure that "db.<current-db>" # has data r.set('foo', 'bar') + + if skip_if_redis_enterprise(None).args[0] is True: + with pytest.raises(redis.exceptions.ResponseError): + stats = r.memory_stats() + return + stats = r.memory_stats() assert isinstance(stats, dict) for key, value in stats.items(): @@ -3614,6 +3659,7 @@ class TestRedisCommands: assert isinstance(r.memory_usage('foo'), int) @skip_if_server_version_lt('4.0.0') + @skip_if_redis_enterprise def test_module_list(self, r): assert isinstance(r.module_list(), list) for x in r.module_list(): @@ -3625,7 +3671,16 @@ 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): with pytest.raises(redis.exceptions.ModuleError) as excinfo: r.module_load('/some/fake/path') @@ -3661,7 +3716,8 @@ class TestRedisCommands: assert r.restore(key2, 0, dumpdata) assert r.ttl(key2) == -1 - # idletime + @skip_if_server_version_lt('5.0.0') + def test_restore_idletime(self, r): key = 'yayakey' r.set(key, 'blee!') dumpdata = r.dump(key) @@ -3669,7 +3725,8 @@ class TestRedisCommands: assert r.restore(key, 0, dumpdata, idletime=5) assert r.get(key) == b'blee!' - # frequency + @skip_if_server_version_lt('5.0.0') + def test_restore_frequency(self, r): key = 'yayakey' r.set(key, 'blee!') dumpdata = r.dump(key) @@ -3678,11 +3735,10 @@ class TestRedisCommands: assert r.get(key) == b'blee!' @skip_if_server_version_lt('5.0.0') + @skip_if_redis_enterprise def test_replicaof(self, r): - with pytest.raises(redis.ResponseError): assert r.replicaof("NO ONE") - assert r.replicaof("NO", "ONE") @@ -3756,6 +3812,7 @@ class TestBinarySave: assert '6' in parsed['allocation_stats'] assert '>=256' in parsed['allocation_stats'] + @skip_if_redis_enterprise def test_large_responses(self, r): "The PythonParser has some special cases for return values > 1MB" # load up 5MB of data into a key diff --git a/tests/test_connection.py b/tests/test_connection.py index f2fc834..7c44768 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 diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py index 6fedec6..521f520 100644 --- a/tests/test_connection_pool.py +++ b/tests/test_connection_pool.py @@ -7,7 +7,11 @@ from unittest import mock from threading import Thread from redis.connection import ssl_available, to_bool -from .conftest import skip_if_server_version_lt, _get_client +from .conftest import ( + skip_if_server_version_lt, + skip_if_redis_enterprise, + _get_client +) from .test_pubsub import wait_for_message @@ -481,6 +485,7 @@ class TestConnection: assert not pool._available_connections[0]._sock @skip_if_server_version_lt('2.8.8') + @skip_if_redis_enterprise def test_busy_loading_disconnects_socket(self, r): """ If Redis raises a LOADING error, the connection should be @@ -491,6 +496,7 @@ class TestConnection: assert not r.connection._sock @skip_if_server_version_lt('2.8.8') + @skip_if_redis_enterprise def test_busy_loading_from_pipeline_immediate_command(self, r): """ BusyLoadingErrors should raise from Pipelines that execute a @@ -506,6 +512,7 @@ class TestConnection: assert not pool._available_connections[0]._sock @skip_if_server_version_lt('2.8.8') + @skip_if_redis_enterprise def test_busy_loading_from_pipeline(self, r): """ BusyLoadingErrors should be raised from a pipeline execution @@ -521,6 +528,7 @@ class TestConnection: assert not pool._available_connections[0]._sock @skip_if_server_version_lt('2.8.8') + @skip_if_redis_enterprise def test_read_only_error(self, r): "READONLY errors get turned in ReadOnlyError exceptions" with pytest.raises(redis.ReadOnlyError): @@ -546,6 +554,7 @@ class TestConnection: 'path=/path/to/socket,db=0', ) + @skip_if_redis_enterprise def test_connect_no_auth_supplied_when_required(self, r): """ AuthenticationError should be raised when the server requires a @@ -555,6 +564,7 @@ class TestConnection: r.execute_command('DEBUG', 'ERROR', 'ERR Client sent AUTH, but no password is set') + @skip_if_redis_enterprise def test_connect_invalid_password_supplied(self, r): "AuthenticationError should be raised when sending the wrong password" with pytest.raises(redis.AuthenticationError): diff --git a/tests/test_monitor.py b/tests/test_monitor.py index bbb7fb7..a8a535b 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -1,4 +1,8 @@ -from .conftest import wait_for_command +from .conftest import ( + skip_if_redis_enterprise, + skip_ifnot_redis_enterprise, + wait_for_command +) class TestMonitor: @@ -40,6 +44,7 @@ class TestMonitor: response = wait_for_command(r, m, 'GET foo\\\\x92') assert response['command'] == 'GET foo\\\\x92' + @skip_if_redis_enterprise def test_lua_script(self, r): with r.monitor() as m: script = 'return redis.call("GET", "foo")' @@ -49,3 +54,11 @@ class TestMonitor: assert response['client_type'] == 'lua' assert response['client_address'] == 'lua' assert response['client_port'] == '' + + @skip_ifnot_redis_enterprise + def test_lua_script_in_enterprise(self, r): + with r.monitor() as m: + script = 'return redis.call("GET", "foo")' + assert r.eval(script, 0) is None + response = wait_for_command(r, m, 'GET foo') + assert response is None diff --git a/tests/test_pubsub.py b/tests/test_pubsub.py index cfc6e5e..e242459 100644 --- a/tests/test_pubsub.py +++ b/tests/test_pubsub.py @@ -7,7 +7,11 @@ import pytest import redis from redis.exceptions import ConnectionError -from .conftest import _get_client, skip_if_server_version_lt +from .conftest import ( + _get_client, + skip_if_redis_enterprise, + skip_if_server_version_lt +) def wait_for_message(pubsub, timeout=0.1, ignore_subscribe_messages=False): @@ -528,6 +532,7 @@ class TestPubSubPings: class TestPubSubConnectionKilled: @skip_if_server_version_lt('3.0.0') + @skip_if_redis_enterprise def test_connection_error_raised_when_connection_dies(self, r): p = r.pubsub() p.subscribe('foo') diff --git a/tests/test_scripting.py b/tests/test_scripting.py index c3c2094..352f3ba 100644 --- a/tests/test_scripting.py +++ b/tests/test_scripting.py @@ -2,6 +2,8 @@ import pytest from redis import exceptions +from tests.conftest import skip_if_server_version_lt + multiply_script = """ local value = redis.call('GET', KEYS[1]) @@ -30,7 +32,8 @@ class TestScripting: # 2 * 3 == 6 assert r.eval(multiply_script, 1, 'a', 3) == 6 - def test_script_flush(self, r): + @skip_if_server_version_lt('6.2.0') + def test_script_flush_620(self, r): r.set('a', 2) r.script_load(multiply_script) r.script_flush('ASYNC') @@ -43,6 +46,12 @@ class TestScripting: r.script_load(multiply_script) r.script_flush() + with pytest.raises(exceptions.DataError): + r.set('a', 2) + r.script_load(multiply_script) + r.script_flush("NOTREAL") + + def test_script_flush(self, r): r.set('a', 2) r.script_load(multiply_script) r.script_flush(None) diff --git a/tests/test_search.py b/tests/test_search.py index e07a61c..d1fc75f 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -612,6 +612,12 @@ def test_explain(client): @pytest.mark.redismod +def test_explaincli(client): + with pytest.raises(NotImplementedError): + client.ft().explain_cli("foo") + + +@pytest.mark.redismod def test_summarize(client): createIndex(client.ft()) waitForIndex(client, "idx") @@ -643,9 +649,6 @@ def test_alias(): index1 = getClient() index2 = getClient() - index1.hset("index1:lonestar", mapping={"name": "lonestar"}) - index2.hset("index2:yogurt", mapping={"name": "yogurt"}) - def1 = IndexDefinition(prefix=["index1:"]) def2 = IndexDefinition(prefix=["index2:"]) @@ -654,6 +657,9 @@ def test_alias(): ftindex1.create_index((TextField("name"),), definition=def1) ftindex2.create_index((TextField("name"),), definition=def2) + index1.hset("index1:lonestar", mapping={"name": "lonestar"}) + index2.hset("index2:yogurt", mapping={"name": "yogurt"}) + res = ftindex1.search("*").docs[0] assert "index1:lonestar" == res.id @@ -127,7 +127,8 @@ exclude = .tox, .venv*, build, + docs/*, dist, docker, venv*, - whitelist.py
\ No newline at end of file + whitelist.py |