pax_global_header00006660000000000000000000000064136214453620014520gustar00rootroot0000000000000052 comment=fc07cf4f779dc3253eeab8215f2dd5655af2ccef fakeredis-1.2.1/000077500000000000000000000000001362144536200134565ustar00rootroot00000000000000fakeredis-1.2.1/.gitignore000066400000000000000000000001561362144536200154500ustar00rootroot00000000000000.commands.json fakeredis.egg-info dump.rdb extras/* .tox *.pyc .idea .hypothesis .coverage cover/ venv/ dist/ fakeredis-1.2.1/.travis.yml000066400000000000000000000012121362144536200155630ustar00rootroot00000000000000language: python sudo: false dist: bionic env: - REDIS_PY=3.4.1 python: - 3.8 jobs: include: - python: 3.5 - python: 3.6 - python: 3.7 - python: pypy3 - env: REDIS_PY=2.10.6 - env: REDIS_PY=3.0.1 - env: REDIS_PY=3.1.0 - env: REDIS_PY=3.2.1 - env: REDIS_PY=3.3.11 - env: REDIS_PY=3.4.1 cache: - pip services: - redis-server install: - pip install -r requirements.txt - pip install redis==$REDIS_PY - pip install coveralls before_script: - flake8 script: - coverage erase - pytest --cov=fakeredis notifications: email: - js@jamesls.com - bmerry@ska.ac.za after_success: coveralls fakeredis-1.2.1/CONTRIBUTING.rst000066400000000000000000000013221362144536200161150ustar00rootroot00000000000000============ Contributing ============ Contributions are welcome. To ensure that your contributions are accepted please follow these guidelines. * Follow pep8 (Travis will fails builds that don't pass flake8) * If you are adding docstrings, follow pep257 * If you are adding new functionality or fixing a bug, please add tests. * If you are making a large change, consider filing an issue on github first to see if there are any objections to the proposed changes. In general, new features or bug fixes **will not be merged unless they have tests.** This is not only to ensure the correctness of the code, but to also encourage others to expirement without wondering whether or not they are breaking existing code. fakeredis-1.2.1/COPYING000066400000000000000000000050521362144536200145130ustar00rootroot00000000000000Copyright (c) 2011 James Saryerwinnie, 2017-2018 Bruce Merry All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This software contains portions of code from redis-py, which is distributed under the following license: Copyright (c) 2012 Andy McCurdy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. fakeredis-1.2.1/MANIFEST.in000066400000000000000000000000431362144536200152110ustar00rootroot00000000000000include COPYING include README.rst fakeredis-1.2.1/README.rst000066400000000000000000000511471362144536200151550ustar00rootroot00000000000000fakeredis: A fake version of a redis-py ======================================= .. image:: https://secure.travis-ci.org/jamesls/fakeredis.svg?branch=master :target: http://travis-ci.org/jamesls/fakeredis .. image:: https://coveralls.io/repos/jamesls/fakeredis/badge.svg?branch=master :target: https://coveralls.io/r/jamesls/fakeredis fakeredis is a pure-Python implementation of the redis-py python client that simulates talking to a redis server. This was created for a single purpose: **to write unittests**. Setting up redis is not hard, but many times you want to write unittests that do not talk to an external server (such as redis). This module now allows tests to simply use this module as a reasonable substitute for redis. Although fakeredis is pure Python, you will need lupa_ if you want to run Lua scripts (this includes features like ``redis.lock.Lock``, which are implemented in Lua). If you install fakeredis with ``pip install fakeredis[lua]`` it will be automatically installed. .. _lupa: https://pypi.org/project/lupa/ Alternatives ============ Consider using birdisle_ instead of fakeredis. It embeds the redis codebase into a Python extension, so it implements the full redis command set and behaves far more closely to a real redis implementation. The disadvantage is that it currently only works on Linux. .. _birdisle: https://birdisle.readthedocs.io/en/latest/ How to Use ========== The intent is for fakeredis to act as though you're talking to a real redis server. It does this by storing state internally. For example: .. code-block:: python >>> import fakeredis >>> r = fakeredis.FakeStrictRedis() >>> r.set('foo', 'bar') True >>> r.get('foo') 'bar' >>> r.lpush('bar', 1) 1 >>> r.lpush('bar', 2) 2 >>> r.lrange('bar', 0, -1) [2, 1] The state is stored in an instance of `FakeServer`. If one is not provided at construction, a new instance is automatically created for you, but you can explicitly create one to share state: .. code-block:: python >>> import fakeredis >>> server = fakeredis.FakeServer() >>> r1 = fakeredis.FakeStrictRedis(server=server) >>> r1.set('foo', 'bar') True >>> r2 = fakeredis.FakeStrictRedis(server=server) >>> r2.get('foo') 'bar' >>> r2.set('bar', 'baz') True >>> r1.get('bar') 'baz' >>> r2.get('bar') 'baz' It is also possible to mock connection errors so you can effectively test your error handling. Simply set the connected attribute of the server to `False` after initialization. .. code-block:: python >>> import fakeredis >>> server = fakeredis.FakeServer() >>> server.connected = False >>> r = fakeredis.FakeStrictRedis(server=server) >>> r.set('foo', 'bar') ConnectionError: FakeRedis is emulating a connection error. >>> server.connected = True >>> r.set('foo', 'bar') True Fakeredis implements the same interface as `redis-py`_, the popular redis client for python, and models the responses of redis 5.0. Porting to fakeredis 1.0 ======================== Version 1.0 is an almost total rewrite, intended to support redis-py 3.x and improve the Lua scripting emulation. It has a few backwards incompatibilities that may require changes to your code: 1. By default, each FakeRedis or FakeStrictRedis instance contains its own state. This is equivalent to the `singleton=True` option to previous versions of fakeredis. This change was made to improve isolation between tests. If you need to share state between instances, create a FakeServer, as described above. 2. FakeRedis is now a subclass of FakeStrictRedis, and similarly FakeStrictRedis is a subclass of StrictRedis. Code that uses `isinstance` may behave differently. 3. The `connected` attribute is now a property of `FakeServer`, rather than `FakeRedis` or `FakeStrictRedis`. You can still pass the property to the constructor of the latter (provided no server is provided). Unimplemented Commands ====================== All of the redis commands are implemented in fakeredis with these exceptions: connection ---------- * auth * quit server ------ * bgrewriteaof * client id * client kill * client list * client getname * client pause * client reply * client setname * client unblock * command * command count * command getkeys * command info * config get * config rewrite * config set * config resetstat * debug object * debug segfault * info * memory doctor * memory help * memory malloc-stats * memory purge * memory stats * memory usage * monitor * role * shutdown * slaveof * replicaof * slowlog * sync * time string ------ * bitfield * bitop * bitpos sorted_set ---------- * bzpopmin * bzpopmax * zpopmax * zpopmin cluster ------- * cluster addslots * cluster count-failure-reports * cluster countkeysinslot * cluster delslots * cluster failover * cluster forget * cluster getkeysinslot * cluster info * cluster keyslot * cluster meet * cluster nodes * cluster replicate * cluster reset * cluster saveconfig * cluster set-config-epoch * cluster setslot * cluster slaves * cluster replicas * cluster slots * readonly * readwrite generic ------- * dump * migrate * object * restore * touch * wait geo --- * geoadd * geohash * geopos * geodist * georadius * georadiusbymember pubsub ------ * pubsub scripting --------- * script debug * script exists * script flush * script kill stream ------ * xinfo * xadd * xtrim * xdel * xrange * xrevrange * xlen * xread * xgroup * xreadgroup * xack * xclaim * xpending Other limitations ================= Apart from unimplemented commands, there are a number of cases where fakeredis won't give identical results to real redis. The following are differences that are unlikely to ever be fixed; there are also differences that are fixable (such as commands that do not support all features) which should be filed as bugs in Github. 1. Hyperloglogs are implemented using sets underneath. This means that the `type` command will return the wrong answer, you can't use `get` to retrieve the encoded value, and counts will be slightly different (they will in fact be exact). 2. When a command has multiple error conditions, such as operating on a key of the wrong type and an integer argument is not well-formed, the choice of error to return may not match redis. 3. The `incrbyfloat` and `hincrbyfloat` commands in redis use the C `long double` type, which typically has more precision than Python's `float` type. 4. Redis makes guarantees about the order in which clients blocked on blocking commands are woken up. Fakeredis does not honour these guarantees. 5. Where redis contains bugs, fakeredis generally does not try to provide exact bug-compatibility. It's not practical for fakeredis to try to match the set of bugs in your specific version of redis. 6. There are a number of cases where the behaviour of redis is undefined, such as the order of elements returned by set and hash commands. Fakeredis will generally not produce the same results, and in Python versions before 3.6 may produce different results each time the process is re-run. 7. SCAN/ZSCAN/HSCAN/SSCAN will not necessary iterate all items if items are deleted or renamed during iteration. They also won't necessarily iterate in the same chunk sizes or the same order as redis. Contributing ============ Contributions are welcome. Please see the `contributing guide`_ for more details. The maintainer generally has very little time to work on fakeredis, so the best way to get a bug fixed is to contribute a pull request. If you'd like to help out, you can start with any of the issues labeled with `HelpWanted`_. Running the Tests ================= To ensure parity with the real redis, there are a set of integration tests that mirror the unittests. For every unittest that is written, the same test is run against a real redis instance using a real redis-py client instance. In order to run these tests you must have a redis server running on localhost, port 6379 (the default settings). The integration tests use db=10 in order to minimize collisions with an existing redis instance. To run all the tests, install the requirements file:: pip install -r requirements.txt If you just want to run the unittests:: pytest test_fakeredis.py::TestFakeStrictRedis test_fakeredis.py::TestFakeRedis Because this module is attempting to provide the same interface as `redis-py`_, the python bindings to redis, a reasonable way to test this to to take each unittest and run it against a real redis server. fakeredis and the real redis server should give the same result. This ensures parity between the two. You can run these "integration" tests like this:: pytest test_fakeredis.py::TestRealStrictRedis test_fakeredis.py::TestRealRedis test_fakeredis_hypothesis.py In terms of implementation, ``TestRealRedis`` is a subclass of ``TestFakeRedis`` that overrides a factory method to create an instance of ``redis.Redis`` (an actual python client for redis) instead of ``fakeredis.FakeStrictRedis``. To run both the unittests and the "integration" tests, run:: pytest If redis is not running and you try to run tests against a real redis server, these tests will have a result of 'S' for skipped. There are some tests that test redis blocking operations that are somewhat slow. If you want to skip these tests during day to day development, they have all been tagged as 'slow' so you can skip them by running:: pytest -m "not slow" test_fakeredis.py Revision history ================ 1.2.1 ----- - `#262 `_ Cannot repr redis object without host attribute - Fix a bug in the hypothesis test framework that occasionally caused a failure 1.2.0 ----- - Drop support for Python 2.7. - Test with Python 3.8 and Pypy3. - Refactor Hypothesis-based tests to support the latest version of Hypothesis. - Fix a number of bugs in the Hypothesis tests that were causing spurious test failures or hangs. - Fix some obscure corner cases - If a WATCHed key is MOVEd, don't invalidate the transaction. - Some cases of passing a key of the wrong type to SINTER/SINTERSTORE were not reporting a WRONGTYPE error. - ZUNIONSTORE/ZINTERSTORE could generate different scores from real redis in corner cases (mostly involving infinities). - Speed up the implementation of BINCOUNT. 1.1.1 ----- - Support redis-py 3.4. 1.1.0 ----- - `#257 `_ Add other inputs for redis connection 1.0.5 ----- - `#247 `_ Support NX/XX/CH flags in ZADD command - `#250 `_ Implement UNLINK command - `#252 `_ Fix implementation of ZSCAN 1.0.4 ----- - `#240 `_ `#242 `_ Support for ``redis==3.3`` 1.0.3 ----- - `#235 `_ Support for ``redis==3.2`` 1.0.2 ----- - `#235 `_ Depend on ``redis<3.2`` 1.0.1 ----- - Fix crash when a connection closes without unsubscribing and there is a subsequent PUBLISH 1.0 --- Version 1.0 is a major rewrite. It works at the redis protocol level, rather than at the redis-py level. This allows for many improvements and bug fixes. - `#225 `_ Support redis-py 3.0 - `#65 `_ Support `execute_command` method - `#206 `_ Drop Python 2.6 support - `#141 `_ Support strings in integer arguments - `#218 `_ Watches checks commands rather than final value - `#220 `_ Better support for calling into redis from Lua - `#158 `_ Better timestamp handling - Support for `register_script` function. - Fixes for race conditions caused by keys expiring mid-command - Disallow certain commands in scripts - Fix handling of blocking commands inside transactions - Fix handling of PING inside pubsub connections It also has new unit tests based on hypothesis_, which has identified many corner cases that are now handled correctly. .. _hypothesis: https://hypothesis.readthedocs.io/en/latest/ 1.0rc1 ------ Compared to 1.0b1: - `#231 `_ Fix setup.py, fakeredis is directory/package now - Fix some corner case handling of +0 vs -0 - Fix pubsub `get_message` with a timeout - Disallow certain commands in scripts - Fix handling of blocking commands inside transactions - Fix handling of PING inside pubsub connections - Make hypothesis tests skip if redis is not running - Minor optimisations to zset 1.0b1 ----- Version 1.0 is a major rewrite. It works at the redis protocol level, rather than at the redis-py level. This allows for many improvements and bug fixes. - `#225 `_ Support redis-py 3.0 - `#65 `_ Support `execute_command` method - `#206 `_ Drop Python 2.6 support - `#141 `_ Support strings in integer arguments - `#218 `_ Watches checks commands rather than final value - `#220 `_ Better support for calling into redis from Lua - `#158 `_ Better timestamp handling - Support for `register_script` function. - Fixes for race conditions caused by keys expiring mid-command It also has new unit tests based on hypothesis_, which has identified many corner cases that are now handled correctly. .. _hypothesis: https://hypothesis.readthedocs.io/en/latest/ 0.16.0 ------ - `#224 `_ Add __delitem__ - Restrict to redis<3 0.15.0 ------ - `#219 `_ Add SAVE, BGSAVE and LASTSAVE commands - `#222 `_ Fix deprecation warnings in Python 3.7 0.14.0 ------ This release greatly improves support for threads: the bulk of commands are now thread-safe, ``lock`` has been rewritten to more closely match redis-py, and pubsub now supports ``run_in_thread``: - `#213 `_ pipeline.watch runs transaction even if no commands are queued - `#214 `_ Added pubsub.run_in_thread as it is implemented in redis-py - `#215 `_ Keep pace with redis-py for zrevrange method - `#216 `_ Update behavior of lock to behave closer to redis lock 0.13.1 ------ - `#208 `_ eval's KEYS and ARGV are now lua tables - `#209 `_ Redis operation that returns dict now converted to Lua table when called inside eval operation - `#212 `_ Optimize ``_scan()`` 0.13.0.1 -------- - Fix a typo in the Trove classifiers 0.13.0 ------ - `#202 `_ Function smembers returns deepcopy - `#205 `_ Implemented hstrlen - `#207 `_ Test on Python 3.7 0.12.0 ------ - `#197 `_ Mock connection error - `#195 `_ Align bool/len behaviour of pipeline - `#199 `_ future.types.newbytes does not encode correctly 0.11.0 ------ - `#194 `_ Support ``score_cast_func`` in zset functions - `#192 `_ Make ``__getitem__`` raise a KeyError for missing keys 0.10.3 ------ This is a minor bug-fix release. - `#189 `_ Add 'System' to the list of libc equivalents 0.10.2 ------ This is a bug-fix release. - `#181 `_ Upgrade twine & other packaging dependencies - `#106 `_ randomkey method is not implemented, but is not in the list of unimplemented commands - `#170 `_ Prefer readthedocs.io instead of readthedocs.org for doc links - `#180 `_ zadd with no member-score pairs should fail - `#145 `_ expire / _expire: accept 'long' also as time - `#182 `_ Pattern matching does not match redis behaviour - `#135 `_ Scan includes expired keys - `#185 `_ flushall() doesn't clean everything - `#186 `_ Fix psubscribe with handlers - Run CI on PyPy - Fix coverage measurement 0.10.1 ------ This release merges the fakenewsredis_ fork back into fakeredis. The version number is chosen to be larger than any fakenewsredis release, so version numbers between the forks are comparable. All the features listed under fakenewsredis version numbers below are thus included in fakeredis for the first time in this release. Additionally, the following was added: - `#169 `_ Fix set-bit fakenewsredis 0.10.0 -------------------- - `#14 `_ Add option to create an instance with non-shared data - `#13 `_ Improve emulation of redis -> Lua returns - `#12 `_ Update tox.ini: py35/py36 and extras for eval tests - `#11 `_ Fix typo in private method name fakenewsredis 0.9.5 ------------------- This release makes a start on supporting Lua scripting: - `#9 `_ Add support for StrictRedis.eval for Lua scripts fakenewsredis 0.9.4 ------------------- This is a minor bugfix and optimization release: - `#5 `_ Update to match redis-py 2.10.6 - `#7 `_ Set with invalid expiry time should not set key - Avoid storing useless expiry times in hashes and sorted sets - Improve the performance of bulk zadd fakenewsredis 0.9.3 ------------------- This is a minor bugfix release: - `#6 `_ Fix iteration over pubsub list - `#3 `_ Preserve expiry time when mutating keys - Fixes to typos and broken links in documentation fakenewsredis 0.9.2 ------------------- This is the first release of fakenewsredis, based on fakeredis 0.9.0, with the following features and fixes: - fakeredis `#78 `_ Behaviour of transaction() does not match redis-py - fakeredis `#79 `_ Implement redis-py's .lock() - fakeredis `#90 `_ HINCRBYFLOAT changes hash value type to float - fakeredis `#101 `_ Should raise an error when attempting to get a key holding a list) - fakeredis `#146 `_ Pubsub messages and channel names are forced to be ASCII strings on Python 2 - fakeredis `#163 `_ getset does not to_bytes the value - fakeredis `#165 `_ linsert implementation is incomplete - fakeredis `#128 `_ Remove `_ex_keys` mapping - fakeredis `#139 `_ Fixed all flake8 errors and added flake8 to Travis CI - fakeredis `#166 `_ Add type checking - fakeredis `#168 `_ Use repr to encode floats in to_bytes .. _fakenewsredis: https://github.com/ska-sa/fakenewsredis .. _redis-py: http://redis-py.readthedocs.io/ .. _contributing guide: https://github.com/jamesls/fakeredis/blob/master/CONTRIBUTING.rst .. _HelpWanted: https://github.com/jamesls/fakeredis/issues?q=is%3Aissue+is%3Aopen+label%3AHelpWanted fakeredis-1.2.1/fakeredis/000077500000000000000000000000001362144536200154135ustar00rootroot00000000000000fakeredis-1.2.1/fakeredis/__init__.py000066400000000000000000000001631362144536200175240ustar00rootroot00000000000000from ._server import FakeServer, FakeRedis, FakeStrictRedis, FakeConnection # noqa: F401 __version__ = '1.2.1' fakeredis-1.2.1/fakeredis/_server.py000066400000000000000000002561121362144536200174410ustar00rootroot00000000000000import os import time import threading import math import random import re import warnings import functools import itertools import hashlib import weakref import queue from collections import defaultdict from collections.abc import MutableMapping import six import redis from ._zset import ZSet MAX_STRING_SIZE = 512 * 1024 * 1024 INVALID_EXPIRE_MSG = "invalid expire time in {}" WRONGTYPE_MSG = \ "WRONGTYPE Operation against a key holding the wrong kind of value" SYNTAX_ERROR_MSG = "syntax error" INVALID_INT_MSG = "value is not an integer or out of range" INVALID_FLOAT_MSG = "value is not a valid float" INVALID_OFFSET_MSG = "offset is out of range" INVALID_BIT_OFFSET_MSG = "bit offset is not an integer or out of range" INVALID_BIT_VALUE_MSG = "bit is not an integer or out of range" INVALID_DB_MSG = "DB index is out of range" INVALID_MIN_MAX_FLOAT_MSG = "min or max is not a float" INVALID_MIN_MAX_STR_MSG = "min or max not a valid string range item" STRING_OVERFLOW_MSG = "string exceeds maximum allowed size (512MB)" OVERFLOW_MSG = "increment or decrement would overflow" NONFINITE_MSG = "increment would produce NaN or Infinity" SCORE_NAN_MSG = "resulting score is not a number (NaN)" INVALID_SORT_FLOAT_MSG = "One or more scores can't be converted into double" SRC_DST_SAME_MSG = "source and destination objects are the same" NO_KEY_MSG = "no such key" INDEX_ERROR_MSG = "index out of range" ZADD_NX_XX_ERROR_MSG = "ZADD allows either 'nx' or 'xx', not both" ZUNIONSTORE_KEYS_MSG = "at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE" WRONG_ARGS_MSG = "wrong number of arguments for '{}' command" UNKNOWN_COMMAND_MSG = "unknown command '{}'" EXECABORT_MSG = "Transaction discarded because of previous errors." MULTI_NESTED_MSG = "MULTI calls can not be nested" WITHOUT_MULTI_MSG = "{0} without MULTI" WATCH_INSIDE_MULTI_MSG = "WATCH inside MULTI is not allowed" NEGATIVE_KEYS_MSG = "Number of keys can't be negative" TOO_MANY_KEYS_MSG = "Number of keys can't be greater than number of args" TIMEOUT_NEGATIVE_MSG = "timeout is negative" NO_MATCHING_SCRIPT_MSG = "No matching script. Please use EVAL." GLOBAL_VARIABLE_MSG = "Script attempted to set global variables: {}" COMMAND_IN_SCRIPT_MSG = "This Redis command is not allowed from scripts" BAD_SUBCOMMAND_MSG = "Unknown {} subcommand or wrong # of args." BAD_COMMAND_IN_PUBSUB_MSG = \ "only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context" CONNECTION_ERROR_MSG = "FakeRedis is emulating a connection error." FLAG_NO_SCRIPT = 's' # Command not allowed in scripts class SimpleString: def __init__(self, value): assert isinstance(value, bytes) self.value = value class NoResponse: """Returned by pub/sub commands to indicate that no response should be returned""" pass OK = SimpleString(b'OK') QUEUED = SimpleString(b'QUEUED') PONG = SimpleString(b'PONG') BGSAVE_STARTED = SimpleString(b'Background saving started') def null_terminate(s): # Redis uses C functions on some strings, which means they stop at the # first NULL. if b'\0' in s: return s[:s.find(b'\0')] return s def casenorm(s): return null_terminate(s).lower() def casematch(a, b): return casenorm(a) == casenorm(b) def compile_pattern(pattern): """Compile a glob pattern (e.g. for keys) to a bytes regex. fnmatch.fnmatchcase doesn't work for this, because it uses different escaping rules to redis, uses ! instead of ^ to negate a character set, and handles invalid cases (such as a [ without a ]) differently. This implementation was written by studying the redis implementation. """ # It's easier to work with text than bytes, because indexing bytes # doesn't behave the same in Python 3. Latin-1 will round-trip safely. pattern = pattern.decode('latin-1') parts = ['^'] i = 0 L = len(pattern) while i < L: c = pattern[i] i += 1 if c == '?': parts.append('.') elif c == '*': parts.append('.*') elif c == '\\': if i == L: i -= 1 parts.append(re.escape(pattern[i])) i += 1 elif c == '[': parts.append('[') if i < L and pattern[i] == '^': i += 1 parts.append('^') parts_len = len(parts) # To detect if anything was added while i < L: if pattern[i] == '\\' and i + 1 < L: i += 1 parts.append(re.escape(pattern[i])) elif pattern[i] == ']': i += 1 break elif i + 2 < L and pattern[i + 1] == '-': start = pattern[i] end = pattern[i + 2] if start > end: start, end = end, start parts.append(re.escape(start) + '-' + re.escape(end)) i += 2 else: parts.append(re.escape(pattern[i])) i += 1 if len(parts) == parts_len: if parts[-1] == '[': # Empty group - will never match parts[-1] = '(?:$.)' else: # Negated empty group - matches any character assert parts[-1] == '^' parts.pop() parts[-1] = '.' else: parts.append(']') else: parts.append(re.escape(c)) parts.append('\\Z') regex = ''.join(parts).encode('latin-1') return re.compile(regex, re.S) class Item: """An item stored in the database""" __slots__ = ['value', 'expireat'] def __init__(self, value): self.value = value self.expireat = None class CommandItem: """An item referenced by a command. It wraps an Item but has extra fields to manage updates and notifications. """ def __init__(self, key, db, item=None, default=None): if item is None: self._value = default self._expireat = None else: self._value = item.value self._expireat = item.expireat self.key = key self.db = db self._modified = False self._expireat_modified = False @property def value(self): return self._value @value.setter def value(self, new_value): self._value = new_value self._modified = True self.expireat = None @property def expireat(self): return self._expireat @expireat.setter def expireat(self, value): self._expireat = value self._expireat_modified = True def get(self, default): return self._value if self else default def update(self, new_value): self._value = new_value self._modified = True def updated(self): self._modified = True def writeback(self): if self._modified: self.db.notify_watch(self.key) if not isinstance(self.value, bytes) and not self.value: self.db.pop(self.key, None) return else: item = self.db.setdefault(self.key, Item(None)) item.value = self.value item.expireat = self.expireat elif self._expireat_modified and self.key in self.db: self.db[self.key].expireat = self.expireat def __bool__(self): return bool(self._value) or isinstance(self._value, bytes) __nonzero__ = __bool__ # For Python 2 class Database(MutableMapping): def __init__(self, lock, *args, **kwargs): self._dict = dict(*args, **kwargs) self.time = 0.0 self._watches = defaultdict(set) # key to set of connections self.condition = threading.Condition(lock) def swap(self, other): self._dict, other._dict = other._dict, self._dict self.time, other.time = other.time, self.time def notify_watch(self, key): for sock in self._watches.get(key, set()): sock.notify_watch() self.condition.notify_all() def add_watch(self, key, sock): self._watches[key].add(sock) def remove_watch(self, key, sock): watches = self._watches[key] watches.discard(sock) if not watches: del self._watches[key] def clear(self): for key in self: self.notify_watch(key) self._dict.clear() def expired(self, item): return item.expireat is not None and item.expireat < self.time def _remove_expired(self): for key in list(self._dict): item = self._dict[key] if self.expired(item): del self._dict[key] def __getitem__(self, key): item = self._dict[key] if self.expired(item): del self._dict[key] raise KeyError(key) return item def __setitem__(self, key, value): self._dict[key] = value def __delitem__(self, key): del self._dict[key] def __iter__(self): self._remove_expired() return iter(self._dict) def __len__(self): self._remove_expired() return len(self._dict) def __hash__(self): return hash(super(object, self)) def __eq__(self, other): return super(object, self) == other class Hash(dict): redis_type = b'hash' class Int: """Argument converter for 64-bit signed integers""" DECODE_ERROR = INVALID_INT_MSG ENCODE_ERROR = OVERFLOW_MSG MIN_VALUE = -2**63 MAX_VALUE = 2**63 - 1 @classmethod def valid(cls, value): return cls.MIN_VALUE <= value <= cls.MAX_VALUE @classmethod def decode(cls, value): try: out = int(value) if not cls.valid(out) or str(out).encode() != value: raise ValueError except ValueError: raise redis.ResponseError(cls.DECODE_ERROR) return out @classmethod def encode(cls, value): if cls.valid(value): return str(value).encode() else: raise redis.ResponseError(cls.ENCODE_ERROR) class BitOffset(Int): """Argument converter for unsigned bit positions""" DECODE_ERROR = INVALID_BIT_OFFSET_MSG MIN_VALUE = 0 MAX_VALUE = 8 * MAX_STRING_SIZE - 1 # Redis imposes 512MB limit on keys class BitValue(Int): DECODE_ERROR = INVALID_BIT_VALUE_MSG MIN_VALUE = 0 MAX_VALUE = 1 class DbIndex(Int): """Argument converter for database indices""" DECODE_ERROR = INVALID_DB_MSG MIN_VALUE = 0 MAX_VALUE = 15 class Timeout(Int): """Argument converter for timeouts""" DECODE_ERROR = TIMEOUT_NEGATIVE_MSG MIN_VALUE = 0 class Float: """Argument converter for floating-point values. Redis uses long double for some cases (INCRBYFLOAT, HINCRBYFLOAT) and double for others (zset scores), but Python doesn't support long double. """ DECODE_ERROR = INVALID_FLOAT_MSG @classmethod def decode(cls, value, allow_leading_whitespace=False, allow_erange=False, allow_empty=False, crop_null=False): # redis has some quirks in float parsing, with several variants. # See https://github.com/antirez/redis/issues/5706 try: if crop_null: value = null_terminate(value) if allow_empty and value == b'': value = b'0.0' if not allow_leading_whitespace and value[:1].isspace(): raise ValueError if value[-1:].isspace(): raise ValueError out = float(value) if math.isnan(out): raise ValueError if not allow_erange: # Values that over- or underflow- are explicitly rejected by # redis. This is a crude hack to determine whether the input # may have been such a value. if out in (math.inf, -math.inf, 0.0) and re.match(b'^[^a-zA-Z]*[1-9]', value): raise ValueError return out except ValueError: raise redis.ResponseError(cls.DECODE_ERROR) @classmethod def encode(cls, value, humanfriendly): if math.isinf(value): return str(value).encode() elif humanfriendly: # Algorithm from ld2string in redis out = '{:.17f}'.format(value) out = re.sub(r'(?:\.)?0+$', '', out) return out.encode() else: return '{:.17g}'.format(value).encode() class SortFloat(Float): DECODE_ERROR = INVALID_SORT_FLOAT_MSG @classmethod def decode(cls, value): return super().decode( value, allow_leading_whitespace=True, allow_empty=True, crop_null=True) class ScoreTest: """Argument converter for sorted set score endpoints.""" def __init__(self, value, exclusive=False): self.value = value self.exclusive = exclusive @classmethod def decode(cls, value): try: exclusive = False if value[:1] == b'(': exclusive = True value = value[1:] value = Float.decode( value, allow_leading_whitespace=True, allow_erange=True, allow_empty=True, crop_null=True) return cls(value, exclusive) except redis.ResponseError: raise redis.ResponseError(INVALID_MIN_MAX_FLOAT_MSG) def __str__(self): if self.exclusive: return '({!r}'.format(self.value) else: return repr(self.value) @property def lower_bound(self): return (self.value, AfterAny() if self.exclusive else BeforeAny()) @property def upper_bound(self): return (self.value, BeforeAny() if self.exclusive else AfterAny()) class StringTest: """Argument converter for sorted set LEX endpoints.""" def __init__(self, value, exclusive): self.value = value self.exclusive = exclusive @classmethod def decode(cls, value): if value == b'-': return cls(BeforeAny(), True) elif value == b'+': return cls(AfterAny(), True) elif value[:1] == b'(': return cls(value[1:], True) elif value[:1] == b'[': return cls(value[1:], False) else: raise redis.ResponseError(INVALID_MIN_MAX_STR_MSG) @functools.total_ordering class BeforeAny: def __gt__(self, other): return False def __eq__(self, other): return isinstance(other, BeforeAny) @functools.total_ordering class AfterAny: def __lt__(self, other): return False def __eq__(self, other): return isinstance(other, AfterAny) class Key: """Marker to indicate that argument in signature is a key""" UNSPECIFIED = object() def __init__(self, type_=None, missing_return=UNSPECIFIED): self.type_ = type_ self.missing_return = missing_return class Signature: def __init__(self, name, fixed, repeat=(), flags=""): self.name = name self.fixed = fixed self.repeat = repeat self.flags = flags def check_arity(self, args): if len(args) != len(self.fixed): delta = len(args) - len(self.fixed) if delta < 0 or not self.repeat: raise redis.ResponseError(WRONG_ARGS_MSG.format(self.name)) def apply(self, args, db): """Returns a tuple, which is either: - transformed args and a dict of CommandItems; or - a single containing a short-circuit return value """ self.check_arity(args) if self.repeat: delta = len(args) - len(self.fixed) if delta % len(self.repeat) != 0: raise redis.ResponseError(WRONG_ARGS_MSG.format(self.name)) types = list(self.fixed) for i in range(len(args) - len(types)): types.append(self.repeat[i % len(self.repeat)]) args = list(args) # First pass: convert/validate non-keys, and short-circuit on missing keys for i, (arg, type_) in enumerate(zip(args, types)): if isinstance(type_, Key): if type_.missing_return is not Key.UNSPECIFIED and arg not in db: return (type_.missing_return,) elif type_ != bytes: args[i] = type_.decode(args[i]) # Second pass: read keys and check their types command_items = [] for i, (arg, type_) in enumerate(zip(args, types)): if isinstance(type_, Key): item = db.get(arg) default = None if type_.type_ is not None: if item is not None and type(item.value) != type_.type_: raise redis.ResponseError(WRONGTYPE_MSG) if item is None: if type_.type_ is not bytes: default = type_.type_() args[i] = CommandItem(arg, db, item, default=default) command_items.append(args[i]) return args, command_items def valid_response_type(value, nested=False): if isinstance(value, NoResponse) and not nested: return True if value is not None and not isinstance(value, (bytes, SimpleString, redis.ResponseError, int, list)): return False if isinstance(value, list): if any(not valid_response_type(item, True) for item in value): return False return True def command(*args, **kwargs): def decorator(func): name = kwargs.pop('name', func.__name__) func._fakeredis_sig = Signature(name, *args, **kwargs) return func return decorator class FakeServer: def __init__(self): self.lock = threading.Lock() self.dbs = defaultdict(lambda: Database(self.lock)) # Maps SHA1 to script source self.script_cache = {} # Maps channel/pattern to weak set of sockets self.subscribers = defaultdict(weakref.WeakSet) self.psubscribers = defaultdict(weakref.WeakSet) self.lastsave = int(time.time()) self.connected = True class FakeSocket: def __init__(self, server): self._server = server self._db = server.dbs[0] self._db_num = 0 # When in a MULTI, set to a list of function calls self._transaction = None self._transaction_failed = False # Set when executing the commands from EXEC self._in_transaction = False self._watch_notified = False self._watches = set() self._pubsub = 0 # Count of subscriptions self.responses = queue.Queue() self._parser = self._parse_commands() self._parser.send(None) def shutdown(self, flags): self._parser.close() def fileno(self): # Our fake socket must return an integer from `FakeSocket.fileno()` since a real selector # will be created. The value does not matter since we replace the selector with our own # `FakeSelector` before it is ever used. return 0 def close(self): with self._server.lock: for subs in self._server.subscribers.values(): subs.discard(self) for subs in self._server.psubscribers.values(): subs.discard(self) self._clear_watches() self._server = None self._db = None self.responses = None @staticmethod def _extract_line(buf): pos = buf.find(b'\n') + 1 assert pos > 0 line = buf[:pos] buf = buf[pos:] assert line.endswith(b'\r\n') return line, buf def _parse_commands(self): """Generator that parses commands. It is fed pieces of redis protocol data (via `send`) and calls `_process_command` whenever it has a complete one. """ buf = b'' while True: while b'\n' not in buf: buf += yield line, buf = self._extract_line(buf) assert line[:1] == b'*' # array n_fields = int(line[1:-2]) fields = [] for i in range(n_fields): while b'\n' not in buf: buf += yield line, buf = self._extract_line(buf) assert line[:1] == b'$' # string length = int(line[1:-2]) while len(buf) < length + 2: buf += yield fields.append(buf[:length]) buf = buf[length+2:] # +2 to skip the CRLF self._process_command(fields) def _run_command(self, func, sig, args, from_script): command_items = {} try: ret = sig.apply(args, self._db) if len(ret) == 1: result = ret[0] else: args, command_items = ret if from_script and FLAG_NO_SCRIPT in sig.flags: raise redis.ResponseError(COMMAND_IN_SCRIPT_MSG) if self._pubsub and sig.name not in [ 'ping', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe', 'quit']: raise redis.ResponseError(BAD_COMMAND_IN_PUBSUB_MSG) result = func(*args) assert valid_response_type(result) except redis.ResponseError as exc: result = exc for command_item in command_items: command_item.writeback() return result def _decode_result(self, result): """Turn SimpleString into native string and int into long, recursively""" if isinstance(result, list): return [self._decode_result(r) for r in result] elif isinstance(result, SimpleString): return result.value else: return result def _blocking(self, timeout, func): """Run a function until it succeeds or timeout is reached. The timeout must be an integer, and 0 means infinite. The function is called with a boolean to indicate whether this is the first call. If it returns None it is considered to have "failed" and is retried each time the condition variable is notified, until the timeout is reached. Returns the function return value, or None if the timeout was reached. """ ret = func(True) if ret is not None or self._in_transaction: return ret if timeout: deadline = time.time() + timeout else: deadline = None while True: timeout = deadline - time.time() if deadline is not None else None if timeout is not None and timeout <= 0: return None # Python <3.2 doesn't return a status from wait. On Python 3.2+ # we bail out early on False. if self._db.condition.wait(timeout=timeout) is False: return None # Timeout expired ret = func(False) if ret is not None: return ret def _name_to_func(self, name): name = six.ensure_str(name, encoding='utf-8', errors='replace') func_name = name.lower() func = getattr(self, func_name, None) if name.startswith('_') or not func or not hasattr(func, '_fakeredis_sig'): # redis remaps \r or \n in an error to ' ' to make it legal protocol clean_name = name.replace('\r', ' ').replace('\n', ' ') raise redis.ResponseError(UNKNOWN_COMMAND_MSG.format(clean_name)) return func, func_name def sendall(self, data): if not self._server.connected: raise redis.ConnectionError(CONNECTION_ERROR_MSG) data = six.ensure_binary(data, encoding='ascii') self._parser.send(data) def _process_command(self, fields): if not fields: return try: func, func_name = self._name_to_func(fields[0]) sig = func._fakeredis_sig with self._server.lock: now = time.time() for db in self._server.dbs.values(): db.time = now sig.check_arity(fields[1:]) # TODO: make a signature attribute for transactions if self._transaction is not None \ and func_name not in ('exec', 'discard', 'multi', 'watch'): self._transaction.append((func, sig, fields[1:])) result = QUEUED else: result = self._run_command(func, sig, fields[1:], False) except redis.ResponseError as exc: if self._transaction is not None: # TODO: should not apply if the exception is from _run_command # e.g. watch inside multi self._transaction_failed = True result = exc result = self._decode_result(result) if not isinstance(result, NoResponse): self.responses.put(result) def notify_watch(self): self._watch_notified = True # redis has inconsistent handling of negative indices, hence two versions # of this code. @staticmethod def _fix_range_string(start, end, length): # Negative number handling is based on the redis source code if start < 0 and end < 0 and start > end: return -1, -1 if start < 0: start = max(0, start + length) if end < 0: end = max(0, end + length) end = min(end, length - 1) return start, end + 1 @staticmethod def _fix_range(start, end, length): # Redis handles negative slightly differently for zrange if start < 0: start = max(0, start + length) if end < 0: end += length if start > end or start >= length: return -1, -1 end = min(end, length - 1) return start, end + 1 def _scan(self, keys, cursor, *args): """ This is the basis of most of the ``scan`` methods. This implementation is KNOWN to be un-performant, as it requires grabbing the full set of keys over which we are investigating subsets. It also doesn't adhere to the guarantee that every key will be iterated at least once even if the database is modified during the scan. However, provided the database is not modified, every key will be returned exactly once. """ pattern = None count = 10 if len(args) % 2 != 0: raise redis.ResponseError(SYNTAX_ERROR_MSG) for i in range(0, len(args), 2): if casematch(args[i], b'match'): pattern = args[i + 1] elif casematch(args[i], b'count'): count = Int.decode(args[i + 1]) if count <= 0: raise redis.ResponseError(SYNTAX_ERROR_MSG) else: raise redis.ResponseError(SYNTAX_ERROR_MSG) if cursor >= len(keys): return [0, []] data = sorted(keys) result_cursor = cursor + count result_data = [] if pattern is not None: regex = compile_pattern(pattern) for val in itertools.islice(data, cursor, result_cursor): compare_val = val[0] if isinstance(val, tuple) else val if regex.match(compare_val): result_data.append(val) else: result_data = data[cursor:result_cursor] if result_cursor >= len(data): result_cursor = 0 return [result_cursor, result_data] # Connection commands # TODO: auth, quit @command((bytes,)) def echo(self, message): return message @command((), (bytes,)) def ping(self, *args): if len(args) > 1: raise redis.ResponseError(WRONG_ARGS_MSG.format('ping')) if self._pubsub: return [b'pong', args[0] if args else b''] else: return args[0] if args else PONG @command((DbIndex,)) def select(self, index): self._db = self._server.dbs[index] self._db_num = index return OK @command((DbIndex, DbIndex)) def swapdb(self, index1, index2): if index1 != index2: db1 = self._server.dbs[index1] db2 = self._server.dbs[index2] db1.swap(db2) return OK # Key commands # TODO: lots def _delete(self, *keys): ans = 0 done = set() for key in keys: if key and key.key not in done: key.value = None done.add(key.key) ans += 1 return ans @command((Key(),), (Key(),), name='del') def del_(self, *keys): return self._delete(*keys) @command((Key(),), (Key(),), name='unlink') def unlink(self, *keys): return self._delete(*keys) @command((Key(),), (Key(),)) def exists(self, *keys): ret = 0 for key in keys: if key: ret += 1 return ret def _expireat(self, key, timestamp): if not key: return 0 else: key.expireat = timestamp return 1 def _ttl(self, key, scale): if not key: return -2 elif key.expireat is None: return -1 else: return int(round((key.expireat - self._db.time) * scale)) @command((Key(), Int)) def expire(self, key, seconds): return self._expireat(key, self._db.time + seconds) @command((Key(), Int)) def expireat(self, key, timestamp): return self._expireat(key, float(timestamp)) @command((Key(), Int)) def pexpire(self, key, ms): return self._expireat(key, self._db.time + ms / 1000.0) @command((Key(), Int)) def pexpireat(self, key, ms_timestamp): return self._expireat(key, ms_timestamp / 1000.0) @command((Key(),)) def ttl(self, key): return self._ttl(key, 1.0) @command((Key(),)) def pttl(self, key): return self._ttl(key, 1000.0) @command((Key(),)) def type(self, key): if key.value is None: return SimpleString(b'none') elif isinstance(key.value, bytes): return SimpleString(b'string') elif isinstance(key.value, list): return SimpleString(b'list') elif isinstance(key.value, set): return SimpleString(b'set') elif isinstance(key.value, ZSet): return SimpleString(b'zset') elif isinstance(key.value, dict): return SimpleString(b'hash') else: assert False # pragma: nocover @command((Key(),)) def persist(self, key): if key.expireat is None: return 0 key.expireat = None return 1 @command((bytes,)) def keys(self, pattern): if pattern == b'*': return list(self._db) else: regex = compile_pattern(pattern) return [key for key in self._db if regex.match(key)] @command((Key(), DbIndex)) def move(self, key, db): if db == self._db_num: raise redis.ResponseError(SRC_DST_SAME_MSG) if not key or key.key in self._server.dbs[db]: return 0 # TODO: what is the interaction with expiry? self._server.dbs[db][key.key] = self._server.dbs[self._db_num].pop(key.key) return 1 @command(()) def randomkey(self): keys = list(self._db.keys()) if not keys: return None return random.choice(keys) @command((Key(), Key())) def rename(self, key, newkey): if not key: raise redis.ResponseError(NO_KEY_MSG) # TODO: check interaction with WATCH if newkey.key != key.key: newkey.value = key.value newkey.expireat = key.expireat key.value = None return OK @command((Key(), Key())) def renamenx(self, key, newkey): if not key: raise redis.ResponseError(NO_KEY_MSG) if newkey: return 0 self.rename(key, newkey) return 1 @command((Int,), (bytes, bytes)) def scan(self, cursor, *args): return self._scan(list(self._db), cursor, *args) def _lookup_key(self, key, pattern): """Python implementation of lookupKeyByPattern from redis""" if pattern == b'#': return key p = pattern.find(b'*') if p == -1: return None prefix = pattern[:p] suffix = pattern[p+1:] arrow = suffix.find(b'->', 0, -1) if arrow != -1: field = suffix[arrow+2:] suffix = suffix[:arrow] else: field = None new_key = prefix + key + suffix item = CommandItem(new_key, self._db, item=self._db.get(new_key)) if item.value is None: return None if field is not None: if not isinstance(item.value, dict): return None return item.value.get(field) else: if not isinstance(item.value, bytes): return None return item.value @command((Key(),), (bytes,)) def sort(self, key, *args): i = 0 desc = False alpha = False limit_start = 0 limit_count = -1 store = None sortby = None dontsort = False get = [] if key.value is not None: if not isinstance(key.value, (set, list, ZSet)): raise redis.ResponseError(WRONGTYPE_MSG) while i < len(args): arg = args[i] if casematch(arg, b'asc'): desc = False elif casematch(arg, b'desc'): desc = True elif casematch(arg, b'alpha'): alpha = True elif casematch(arg, b'limit') and i + 2 < len(args): try: limit_start = Int.decode(args[i + 1]) limit_count = Int.decode(args[i + 2]) except redis.ResponseError: raise redis.ResponseError(SYNTAX_ERROR_MSG) else: i += 2 elif casematch(arg, b'store') and i + 1 < len(args): store = args[i + 1] i += 1 elif casematch(arg, b'by') and i + 1 < len(args): sortby = args[i + 1] if b'*' not in sortby: dontsort = True i += 1 elif casematch(arg, b'get') and i + 1 < len(args): get.append(args[i + 1]) i += 1 else: raise redis.ResponseError(SYNTAX_ERROR_MSG) i += 1 # TODO: force sorting if the object is a set and either in Lua or # storing to a key, to match redis behaviour. items = list(key.value) if key.value is not None else [] # These transformations are based on the redis implementation, but # changed to produce a half-open range. start = max(limit_start, 0) end = len(items) if limit_count < 0 else start + limit_count if start >= len(items): start = end = len(items) - 1 end = min(end, len(items)) if not get: get.append(b'#') if sortby is None: sortby = b'#' if not dontsort: if alpha: def sort_key(v): byval = self._lookup_key(v, sortby) # TODO: use locale.strxfrm when not storing? But then need # to decode too. if byval is None: byval = BeforeAny() return byval else: def sort_key(v): byval = self._lookup_key(v, sortby) score = SortFloat.decode(byval) if byval is not None else 0.0 return (score, v) items.sort(key=sort_key, reverse=desc) elif isinstance(key.value, (list, ZSet)): items.reverse() out = [] for row in items[start:end]: for g in get: v = self._lookup_key(row, g) if store is not None and v is None: v = b'' out.append(v) if store is not None: item = CommandItem(store, self._db, item=self._db.get(store)) item.value = out item.writeback() return len(out) else: return out # Transaction commands def _clear_watches(self): self._watch_notified = False while self._watches: (key, db) = self._watches.pop() db.remove_watch(key, self) @command((), flags='s') def multi(self): if self._transaction is not None: raise redis.ResponseError(MULTI_NESTED_MSG) self._transaction = [] self._transaction_failed = False return OK @command((), flags='s') def discard(self): if self._transaction is None: raise redis.ResponseError(WITHOUT_MULTI_MSG.format('DISCARD')) self._transaction = None self._transaction_failed = False self._clear_watches() return OK @command((), name='exec', flags='s') def exec_(self): if self._transaction is None: raise redis.ResponseError(WITHOUT_MULTI_MSG.format('EXEC')) if self._transaction_failed: self._transaction = None raise redis.exceptions.ExecAbortError(EXECABORT_MSG) transaction = self._transaction self._transaction = None self._transaction_failed = False watch_notified = self._watch_notified self._clear_watches() if watch_notified: return None result = [] for func, sig, args in transaction: try: self._in_transaction = True ans = self._run_command(func, sig, args, False) except redis.ResponseError as exc: ans = exc finally: self._in_transaction = False result.append(ans) return result @command((Key(),), (Key(),), flags='s') def watch(self, *keys): if self._transaction is not None: raise redis.ResponseError(WATCH_INSIDE_MULTI_MSG) for key in keys: if key not in self._watches: self._watches.add((key.key, self._db)) self._db.add_watch(key.key, self) return OK @command((), flags='s') def unwatch(self): self._clear_watches() return OK # String commands # TODO: bitfield, bitop, bitpos @command((Key(bytes), bytes)) def append(self, key, value): old = key.get(b'') if len(old) + len(value) > MAX_STRING_SIZE: raise redis.ResponseError(STRING_OVERFLOW_MSG) key.update(key.get(b'') + value) return len(key.value) @command((Key(bytes, 0),), (bytes,)) def bitcount(self, key, *args): # Redis checks the argument count before decoding integers. That's why # we can't declare them as Int. if args: if len(args) != 2: raise redis.ResponseError(SYNTAX_ERROR_MSG) start = Int.decode(args[0]) end = Int.decode(args[1]) start, end = self._fix_range_string(start, end, len(key.value)) value = key.value[start:end] else: value = key.value return bin(int.from_bytes(value, 'little')).count('1') @command((Key(bytes), Int)) def decrby(self, key, amount): return self.incrby(key, -amount) @command((Key(bytes),)) def decr(self, key): return self.incrby(key, -1) @command((Key(bytes), Int)) def incrby(self, key, amount): c = Int.decode(key.get(b'0')) + amount key.update(Int.encode(c)) return c @command((Key(bytes),)) def incr(self, key): return self.incrby(key, 1) @command((Key(bytes), bytes)) def incrbyfloat(self, key, amount): # TODO: introduce convert_order so that we can specify amount is Float c = Float.decode(key.get(b'0')) + Float.decode(amount) if not math.isfinite(c): raise redis.ResponseError(NONFINITE_MSG) encoded = Float.encode(c, True) key.update(encoded) return encoded @command((Key(bytes),)) def get(self, key): return key.get(None) @command((Key(bytes), BitOffset)) def getbit(self, key, offset): value = key.get(b'') byte = offset // 8 remaining = offset % 8 actual_bitoffset = 7 - remaining try: actual_val = value[byte] except IndexError: return 0 return 1 if (1 << actual_bitoffset) & actual_val else 0 @command((Key(bytes), BitOffset, BitValue)) def setbit(self, key, offset, value): val = key.get(b'\x00') byte = offset // 8 remaining = offset % 8 actual_bitoffset = 7 - remaining if len(val) - 1 < byte: # We need to expand val so that we can set the appropriate # bit. needed = byte - (len(val) - 1) val += b'\x00' * needed old_byte = val[byte] if value == 1: new_byte = old_byte | (1 << actual_bitoffset) else: new_byte = old_byte & ~(1 << actual_bitoffset) old_value = value if old_byte == new_byte else 1 - value reconstructed = bytearray(val) reconstructed[byte] = new_byte key.update(bytes(reconstructed)) return old_value @command((Key(bytes), Int, Int)) def getrange(self, key, start, end): value = key.get(b'') start, end = self._fix_range_string(start, end, len(value)) return value[start:end] # substr is a deprecated alias for getrange @command((Key(bytes), Int, Int)) def substr(self, key, start, end): return self.getrange(key, start, end) @command((Key(bytes), bytes)) def getset(self, key, value): old = key.value key.value = value return old @command((Key(),), (Key(),)) def mget(self, *keys): return [key.value if isinstance(key.value, bytes) else None for key in keys] @command((Key(), bytes), (Key(), bytes)) def mset(self, *args): for i in range(0, len(args), 2): args[i].value = args[i + 1] return OK @command((Key(), bytes), (Key(), bytes)) def msetnx(self, *args): for i in range(0, len(args), 2): if args[i]: return 0 for i in range(0, len(args), 2): args[i].value = args[i + 1] return 1 @command((Key(), bytes), (bytes,), name='set') def set_(self, key, value, *args): i = 0 ex = None px = None xx = False nx = False while i < len(args): if casematch(args[i], b'nx'): nx = True i += 1 elif casematch(args[i], b'xx'): xx = True i += 1 elif casematch(args[i], b'ex') and i + 1 < len(args): ex = Int.decode(args[i + 1]) if ex <= 0: raise redis.ResponseError(INVALID_EXPIRE_MSG.format('set')) i += 2 elif casematch(args[i], b'px') and i + 1 < len(args): px = Int.decode(args[i + 1]) if px <= 0: raise redis.ResponseError(INVALID_EXPIRE_MSG.format('set')) i += 2 else: raise redis.ResponseError(SYNTAX_ERROR_MSG) if (xx and nx) or (px is not None and ex is not None): raise redis.ResponseError(SYNTAX_ERROR_MSG) if nx and key: return None if xx and not key: return None key.value = value if ex is not None: key.expireat = self._db.time + ex if px is not None: key.expireat = self._db.time + px / 1000.0 return OK @command((Key(), Int, bytes)) def setex(self, key, seconds, value): if seconds <= 0: raise redis.ResponseError(INVALID_EXPIRE_MSG.format('setex')) key.value = value key.expireat = self._db.time + seconds return OK @command((Key(), Int, bytes)) def psetex(self, key, ms, value): if ms <= 0: raise redis.ResponseError(INVALID_EXPIRE_MSG.format('psetex')) key.value = value key.expireat = self._db.time + ms / 1000.0 return OK @command((Key(), bytes)) def setnx(self, key, value): if key: return 0 key.value = value return 1 @command((Key(bytes), Int, bytes)) def setrange(self, key, offset, value): if offset < 0: raise redis.ResponseError(INVALID_OFFSET_MSG) elif not value: return len(key.get(b'')) elif offset + len(value) > MAX_STRING_SIZE: raise redis.ResponseError(STRING_OVERFLOW_MSG) else: out = key.get(b'') if len(out) < offset: out += b'\x00' * (offset - len(out)) out = out[0:offset] + value + out[offset+len(value):] key.update(out) return len(out) @command((Key(bytes),)) def strlen(self, key): return len(key.get(b'')) # Hash commands @command((Key(Hash), bytes), (bytes,)) def hdel(self, key, *fields): h = key.value rem = 0 for field in fields: if field in h: del h[field] key.updated() rem += 1 return rem @command((Key(Hash), bytes)) def hexists(self, key, field): return int(field in key.value) @command((Key(Hash), bytes)) def hget(self, key, field): return key.value.get(field) @command((Key(Hash),)) def hgetall(self, key): return list(itertools.chain(*key.value.items())) @command((Key(Hash), bytes, Int)) def hincrby(self, key, field, amount): c = Int.decode(key.value.get(field, b'0')) + amount key.value[field] = Int.encode(c) key.updated() return c @command((Key(Hash), bytes, bytes)) def hincrbyfloat(self, key, field, amount): c = Float.decode(key.value.get(field, b'0')) + Float.decode(amount) if not math.isfinite(c): raise redis.ResponseError(NONFINITE_MSG) encoded = Float.encode(c, True) key.value[field] = encoded key.updated() return encoded @command((Key(Hash),)) def hkeys(self, key): return list(key.value.keys()) @command((Key(Hash),)) def hlen(self, key): return len(key.value) @command((Key(Hash), bytes), (bytes,)) def hmget(self, key, *fields): return [key.value.get(field) for field in fields] @command((Key(Hash), bytes, bytes), (bytes, bytes)) def hmset(self, key, *args): self.hset(key, *args) return OK @command((Key(Hash), Int,), (bytes, bytes)) def hscan(self, key, cursor, *args): cursor, keys = self._scan(key.value, cursor, *args) items = [] for k in keys: items.append(k) items.append(key.value[k]) return [cursor, items] @command((Key(Hash), bytes, bytes), (bytes, bytes)) def hset(self, key, *args): h = key.value created = 0 for i in range(0, len(args), 2): if args[i] not in h: created += 1 h[args[i]] = args[i + 1] key.updated() return created @command((Key(Hash), bytes, bytes)) def hsetnx(self, key, field, value): if field in key.value: return 0 return self.hset(key, field, value) @command((Key(Hash), bytes)) def hstrlen(self, key, field): return len(key.value.get(field, b'')) @command((Key(Hash),)) def hvals(self, key): return list(key.value.values()) # List commands def _bpop_pass(self, keys, op, first_pass): for key in keys: item = CommandItem(key, self._db, item=self._db.get(key), default=[]) if not isinstance(item.value, list): if first_pass: raise redis.ResponseError(WRONGTYPE_MSG) else: continue if item.value: ret = op(item.value) item.updated() item.writeback() return [key, ret] return None def _bpop(self, args, op): keys = args[:-1] timeout = Timeout.decode(args[-1]) return self._blocking(timeout, functools.partial(self._bpop_pass, keys, op)) @command((bytes, bytes), (bytes,), flags='s') def blpop(self, *args): return self._bpop(args, lambda lst: lst.pop(0)) @command((bytes, bytes), (bytes,), flags='s') def brpop(self, *args): return self._bpop(args, lambda lst: lst.pop()) def _brpoplpush_pass(self, source, destination, first_pass): src = CommandItem(source, self._db, item=self._db.get(source), default=[]) if not isinstance(src.value, list): if first_pass: raise redis.ResponseError(WRONGTYPE_MSG) else: return None if not src.value: return None # Empty list dst = CommandItem(destination, self._db, item=self._db.get(destination), default=[]) if not isinstance(dst.value, list): raise redis.ResponseError(WRONGTYPE_MSG) el = src.value.pop() dst.value.insert(0, el) src.updated() src.writeback() if destination != source: # Ensure writeback only happens once dst.updated() dst.writeback() return el @command((bytes, bytes, Timeout), flags='s') def brpoplpush(self, source, destination, timeout): return self._blocking(timeout, functools.partial(self._brpoplpush_pass, source, destination)) @command((Key(list, None), Int)) def lindex(self, key, index): try: return key.value[index] except IndexError: return None @command((Key(list), bytes, bytes, bytes)) def linsert(self, key, where, pivot, value): if not casematch(where, b'before') and not casematch(where, b'after'): raise redis.ResponseError(SYNTAX_ERROR_MSG) if not key: return 0 else: try: index = key.value.index(pivot) except ValueError: return -1 if casematch(where, b'after'): index += 1 key.value.insert(index, value) key.updated() return len(key.value) @command((Key(list),)) def llen(self, key): return len(key.value) @command((Key(list),)) def lpop(self, key): try: ret = key.value.pop(0) key.updated() return ret except IndexError: return None @command((Key(list), bytes), (bytes,)) def lpush(self, key, *values): for value in values: key.value.insert(0, value) key.updated() return len(key.value) @command((Key(list), bytes), (bytes,)) def lpushx(self, key, *values): if not key: return 0 return self.lpush(key, *values) @command((Key(list), Int, Int)) def lrange(self, key, start, stop): start, stop = self._fix_range(start, stop, len(key.value)) return key.value[start:stop] @command((Key(list), Int, bytes)) def lrem(self, key, count, value): a_list = key.value found = [] for i, el in enumerate(a_list): if el == value: found.append(i) if count > 0: indices_to_remove = found[:count] elif count < 0: indices_to_remove = found[count:] else: indices_to_remove = found # Iterating in reverse order to ensure the indices # remain valid during deletion. for index in reversed(indices_to_remove): del a_list[index] if indices_to_remove: key.updated() return len(indices_to_remove) @command((Key(list), Int, bytes)) def lset(self, key, index, value): if not key: raise redis.ResponseError(NO_KEY_MSG) try: key.value[index] = value key.updated() except IndexError: raise redis.ResponseError(INDEX_ERROR_MSG) return OK @command((Key(list), Int, Int)) def ltrim(self, key, start, stop): if key: if stop == -1: stop = None else: stop += 1 new_value = key.value[start:stop] # TODO: check if this should actually be conditional if len(new_value) != len(key.value): key.update(new_value) return OK @command((Key(list),)) def rpop(self, key): try: ret = key.value.pop() key.updated() return ret except IndexError: return None @command((Key(list, None), Key(list))) def rpoplpush(self, src, dst): el = self.rpop(src) self.lpush(dst, el) return el @command((Key(list), bytes), (bytes,)) def rpush(self, key, *values): for value in values: key.value.append(value) key.updated() return len(key.value) @command((Key(list), bytes), (bytes,)) def rpushx(self, key, *values): if not key: return 0 return self.rpush(key, *values) # Set commands @command((Key(set), bytes), (bytes,)) def sadd(self, key, *members): old_size = len(key.value) key.value.update(members) key.updated() return len(key.value) - old_size @command((Key(set),)) def scard(self, key): return len(key.value) def _calc_setop(self, op, stop_if_missing, key, *keys): if stop_if_missing and not key.value: return set() ans = key.value.copy() for other in keys: value = other.value if other.value is not None else set() if not isinstance(value, set): raise redis.ResponseError(WRONGTYPE_MSG) if stop_if_missing and not value: return set() ans = op(ans, value) return ans def _setop(self, op, stop_if_missing, dst, key, *keys): """Apply one of SINTER[STORE], SUNION[STORE], SDIFF[STORE]. If `stop_if_missing`, the output will be made an empty set as soon as an empty input set is encountered (use for SINTER[STORE]). May assume that `key` is a set (or empty), but `keys` could be anything. """ ans = self._calc_setop(op, stop_if_missing, key, *keys) if dst is None: return list(ans) else: dst.value = ans return len(dst.value) @command((Key(set),), (Key(set),)) def sdiff(self, *keys): return self._setop(lambda a, b: a - b, False, None, *keys) @command((Key(), Key(set)), (Key(set),)) def sdiffstore(self, dst, *keys): return self._setop(lambda a, b: a - b, False, dst, *keys) # The following keys can't be marked as sets because of the # stop_if_empty early-out. @command((Key(set),), (Key(),)) def sinter(self, *keys): return self._setop(lambda a, b: a & b, True, None, *keys) @command((Key(), Key(set)), (Key(),)) def sinterstore(self, dst, *keys): return self._setop(lambda a, b: a & b, True, dst, *keys) @command((Key(set), bytes)) def sismember(self, key, member): return int(member in key.value) @command((Key(set),)) def smembers(self, key): return list(key.value) @command((Key(set, 0), Key(set), bytes)) def smove(self, src, dst, member): try: src.value.remove(member) src.updated() except KeyError: return 0 else: dst.value.add(member) dst.updated() # TODO: is it updated if member was already present? return 1 @command((Key(set),), (Int,)) def spop(self, key, count=None): if count is None: if not key.value: return None item = random.sample(key.value, 1)[0] key.value.remove(item) key.updated() return item else: if count < 0: raise redis.ResponseError(INDEX_ERROR_MSG) items = self.srandmember(key, count) for item in items: key.value.remove(item) key.updated() # Inside the loop because redis special-cases count=0 return items @command((Key(set),), (Int,)) def srandmember(self, key, count=None): if count is None: if not key.value: return None else: return random.sample(key.value, 1)[0] elif count >= 0: count = min(count, len(key.value)) return random.sample(key.value, count) else: items = list(key.value) return [random.choice(items) for _ in range(-count)] @command((Key(set), bytes), (bytes,)) def srem(self, key, *members): old_size = len(key.value) for member in members: key.value.discard(member) key.updated() return old_size - len(key.value) @command((Key(set), Int), (bytes, bytes)) def sscan(self, key, cursor, *args): return self._scan(key.value, cursor, *args) @command((Key(set),), (Key(set),)) def sunion(self, *keys): return self._setop(lambda a, b: a | b, False, None, *keys) @command((Key(), Key(set)), (Key(set),)) def sunionstore(self, dst, *keys): return self._setop(lambda a, b: a | b, False, dst, *keys) # Hyperloglog commands # These are not quite the same as the real redis ones, which are # approximate and store the results in a string. Instead, it is implemented # on top of sets. @command((Key(set),), (bytes,)) def pfadd(self, key, *elements): result = self.sadd(key, *elements) # Per the documentation: # - 1 if at least 1 HyperLogLog internal register was altered. 0 otherwise. return 1 if result > 0 else 0 @command((Key(set),), (Key(set),)) def pfcount(self, *keys): """ Return the approximated cardinality of the set observed by the HyperLogLog at key(s). """ return len(self.sunion(*keys)) @command((Key(set), Key(set)), (Key(set),)) def pfmerge(self, dest, *sources): "Merge N different HyperLogLogs into a single one." self.sunionstore(dest, *sources) return OK # Sorted set commands # TODO: [b]zpopmin/zpopmax, @staticmethod def _limit_items(items, offset, count): out = [] for item in items: if offset: # Note: not offset > 0, in order to match redis offset -= 1 continue if count == 0: break count -= 1 out.append(item) return out @staticmethod def _apply_withscores(items, withscores): if withscores: out = [] for item in items: out.append(item[1]) out.append(Float.encode(item[0], False)) else: out = [item[1] for item in items] return out @command((Key(ZSet), bytes, bytes), (bytes,)) def zadd(self, key, *args): # TODO: handle INCR zset = key.value i = 0 ch = False nx = False xx = False while i < len(args): if casematch(args[i], b'ch'): ch = True i += 1 elif casematch(args[i], b'nx'): nx = True i += 1 elif casematch(args[i], b'xx'): xx = True i += 1 else: # First argument not matching flags indicates the start of # score pairs. break if nx and xx: raise redis.ResponseError(ZADD_NX_XX_ERROR_MSG) elements = args[i:] if not elements or len(elements) % 2 != 0: raise redis.ResponseError(SYNTAX_ERROR_MSG) # Parse all scores first, before updating items = [ (Float.decode(elements[j]), elements[j + 1]) for j in range(0, len(elements), 2) ] old_len = len(zset) changed_items = 0 for item_score, item_name in items: if ( (not nx or item_name not in zset) and (not xx or item_name in zset) ): if zset.add(item_name, item_score): changed_items += 1 if changed_items: key.updated() if ch: return changed_items return len(zset) - old_len @command((Key(ZSet),)) def zcard(self, key): return len(key.value) @command((Key(ZSet), ScoreTest, ScoreTest)) def zcount(self, key, min, max): return key.value.zcount(min.lower_bound, max.upper_bound) @command((Key(ZSet), Float, bytes)) def zincrby(self, key, increment, member): # Can't just default the old score to 0.0, because in IEEE754, adding # 0.0 to something isn't a nop (e.g. 0.0 + -0.0 == 0.0). try: score = key.value.get(member, None) + increment except TypeError: score = increment if math.isnan(score): raise redis.ResponseError(SCORE_NAN_MSG) key.value[member] = score key.updated() return Float.encode(score, False) @command((Key(ZSet), StringTest, StringTest)) def zlexcount(self, key, min, max): return key.value.zlexcount(min.value, min.exclusive, max.value, max.exclusive) def _zrange(self, key, start, stop, reverse, *args): zset = key.value # TODO: does redis allow multiple WITHSCORES? if len(args) > 1 or (args and not casematch(args[0], b'withscores')): raise redis.ResponseError(SYNTAX_ERROR_MSG) start, stop = self._fix_range(start, stop, len(zset)) if reverse: start, stop = len(zset) - stop, len(zset) - start items = zset.islice_score(start, stop, reverse) items = self._apply_withscores(items, bool(args)) return items @command((Key(ZSet), Int, Int), (bytes,)) def zrange(self, key, start, stop, *args): return self._zrange(key, start, stop, False, *args) @command((Key(ZSet), Int, Int), (bytes,)) def zrevrange(self, key, start, stop, *args): return self._zrange(key, start, stop, True, *args) def _zrangebylex(self, key, min, max, reverse, *args): if args: if len(args) != 3 or not casematch(args[0], b'limit'): raise redis.ResponseError(SYNTAX_ERROR_MSG) offset = Int.decode(args[1]) count = Int.decode(args[2]) else: offset = 0 count = -1 zset = key.value items = zset.irange_lex(min.value, max.value, inclusive=(not min.exclusive, not max.exclusive), reverse=reverse) items = self._limit_items(items, offset, count) return items @command((Key(ZSet), StringTest, StringTest), (bytes,)) def zrangebylex(self, key, min, max, *args): return self._zrangebylex(key, min, max, False, *args) @command((Key(ZSet), StringTest, StringTest), (bytes,)) def zrevrangebylex(self, key, max, min, *args): return self._zrangebylex(key, min, max, True, *args) def _zrangebyscore(self, key, min, max, reverse, *args): withscores = False offset = 0 count = -1 i = 0 while i < len(args): if casematch(args[i], b'withscores'): withscores = True i += 1 elif casematch(args[i], b'limit') and i + 2 < len(args): offset = Int.decode(args[i + 1]) count = Int.decode(args[i + 2]) i += 3 else: raise redis.ResponseError(SYNTAX_ERROR_MSG) zset = key.value items = list(zset.irange_score(min.lower_bound, max.upper_bound, reverse=reverse)) items = self._limit_items(items, offset, count) items = self._apply_withscores(items, withscores) return items @command((Key(ZSet), ScoreTest, ScoreTest), (bytes,)) def zrangebyscore(self, key, min, max, *args): return self._zrangebyscore(key, min, max, False, *args) @command((Key(ZSet), ScoreTest, ScoreTest), (bytes,)) def zrevrangebyscore(self, key, max, min, *args): return self._zrangebyscore(key, min, max, True, *args) @command((Key(ZSet), bytes)) def zrank(self, key, member): try: return key.value.rank(member) except KeyError: return None @command((Key(ZSet), bytes)) def zrevrank(self, key, member): try: return len(key.value) - 1 - key.value.rank(member) except KeyError: return None @command((Key(ZSet), bytes), (bytes,)) def zrem(self, key, *members): old_size = len(key.value) for member in members: key.value.discard(member) deleted = old_size - len(key.value) if deleted: key.updated() return deleted @command((Key(ZSet), StringTest, StringTest)) def zremrangebylex(self, key, min, max): items = key.value.irange_lex(min.value, max.value, inclusive=(not min.exclusive, not max.exclusive)) return self.zrem(key, *items) @command((Key(ZSet), ScoreTest, ScoreTest)) def zremrangebyscore(self, key, min, max): items = key.value.irange_score(min.lower_bound, max.upper_bound) return self.zrem(key, *[item[1] for item in items]) @command((Key(ZSet), Int, Int)) def zremrangebyrank(self, key, start, stop): zset = key.value start, stop = self._fix_range(start, stop, len(zset)) items = zset.islice_score(start, stop) return self.zrem(key, *[item[1] for item in items]) @command((Key(ZSet), Int), (bytes, bytes)) def zscan(self, key, cursor, *args): new_cursor, ans = self._scan(key.value.items(), cursor, *args) flat = [] for (key, score) in ans: flat.append(key) flat.append(Float.encode(score, False)) return [new_cursor, flat] @command((Key(ZSet), bytes)) def zscore(self, key, member): try: return Float.encode(key.value[member], False) except KeyError: return None @staticmethod def _get_zset(value): if isinstance(value, set): zset = ZSet() for item in value: zset[item] = 1.0 return zset elif isinstance(value, ZSet): return value else: raise redis.ResponseError(WRONGTYPE_MSG) def _zunioninter(self, func, dest, numkeys, *args): if numkeys < 1: raise redis.ResponseError(ZUNIONSTORE_KEYS_MSG) if numkeys > len(args): raise redis.ResponseError(SYNTAX_ERROR_MSG) aggregate = b'sum' sets = [] for i in range(numkeys): item = CommandItem(args[i], self._db, item=self._db.get(args[i]), default=ZSet()) sets.append(self._get_zset(item.value)) weights = [1.0] * numkeys i = numkeys while i < len(args): arg = args[i] if casematch(arg, b'weights') and i + numkeys < len(args): weights = [Float.decode(x) for x in args[i + 1:i + numkeys + 1]] i += numkeys + 1 elif casematch(arg, b'aggregate') and i + 1 < len(args): aggregate = casenorm(args[i + 1]) if aggregate not in (b'sum', b'min', b'max'): raise redis.ResponseError(SYNTAX_ERROR_MSG) i += 2 else: raise redis.ResponseError(SYNTAX_ERROR_MSG) out_members = set(sets[0]) for s in sets[1:]: if func == 'ZUNIONSTORE': out_members |= set(s) else: out_members.intersection_update(s) # We first build a regular dict and turn it into a ZSet. The # reason is subtle: a ZSet won't update a score from -0 to +0 # (or vice versa) through assignment, but a regular dict will. out = {} # The sort affects the order of floating-point operations. # Note that redis uses qsort(1), which has no stability guarantees, # so we can't be sure to match it in all cases. for s, w in sorted(zip(sets, weights), key=lambda x: len(x[0])): for member, score in s.items(): score *= w # Redis only does this step for ZUNIONSTORE. See # https://github.com/antirez/redis/issues/3954. if func == 'ZUNIONSTORE' and math.isnan(score): score = 0.0 if member not in out_members: continue if member in out: old = out[member] if aggregate == b'sum': score += old if math.isnan(score): score = 0.0 elif aggregate == b'max': score = max(old, score) elif aggregate == b'min': score = min(old, score) else: assert False # pragma: nocover if math.isnan(score): score = 0.0 out[member] = score out_zset = ZSet() for member, score in out.items(): out_zset[member] = score dest.value = out_zset return len(out_zset) @command((Key(), Int, bytes), (bytes,)) def zunionstore(self, dest, numkeys, *args): return self._zunioninter('ZUNIONSTORE', dest, numkeys, *args) @command((Key(), Int, bytes), (bytes,)) def zinterstore(self, dest, numkeys, *args): return self._zunioninter('ZINTERSTORE', dest, numkeys, *args) # Server commands # TODO: lots @command((), flags='s') def bgsave(self): self._server.lastsave = int(time.time()) return BGSAVE_STARTED @command(()) def dbsize(self): return len(self._db) @command((), (bytes,)) def flushdb(self, *args): if args: if len(args) != 1 or not casematch(args[0], b'async'): raise redis.ResponseError(SYNTAX_ERROR_MSG) self._db.clear() return OK @command((), (bytes,)) def flushall(self, *args): if args: if len(args) != 1 or not casematch(args[0], b'async'): raise redis.ResponseError(SYNTAX_ERROR_MSG) for db in self._server.dbs.values(): db.clear() # TODO: clear watches and/or pubsub as well? return OK @command(()) def lastsave(self): return self._server.lastsave @command((), flags='s') def save(self): self._server.lastsave = int(time.time()) return OK # Script commands # TODO: script exists, script flush # (script debug and script kill will probably not be supported) def _convert_redis_arg(self, lua_runtime, value): if isinstance(value, bytes): return value elif isinstance(value, (int, float)): return '{:.17g}'.format(value).encode() else: # TODO: add a constant for this, and add the context raise redis.ResponseError('Lua redis() command arguments must be strings or integers') def _convert_redis_result(self, lua_runtime, result): if isinstance(result, (bytes, int)): return result elif isinstance(result, SimpleString): return lua_runtime.table_from({b"ok": result.value}) elif result is None: return False elif isinstance(result, list): converted = [ self._convert_redis_result(lua_runtime, item) for item in result ] return lua_runtime.table_from(converted) elif isinstance(result, redis.ResponseError): raise result else: raise RuntimeError("Unexpected return type from redis: {}".format(type(result))) def _convert_lua_result(self, result, nested=True): from lupa import lua_type if lua_type(result) == 'table': for key in (b'ok', b'err'): if key in result: msg = self._convert_lua_result(result[key]) if not isinstance(msg, bytes): # TODO: put in a constant for this raise redis.ResponseError("wrong number or type of arguments") if key == b'ok': return SimpleString(msg) elif nested: return redis.ResponseError(msg) else: raise redis.ResponseError(msg) # Convert Lua tables into lists, starting from index 1, mimicking the behavior of StrictRedis. result_list = [] for index in itertools.count(1): if index not in result: break item = result[index] result_list.append(self._convert_lua_result(item)) return result_list elif isinstance(result, str): return result.encode() elif isinstance(result, float): return int(result) elif isinstance(result, bool): return 1 if result else None return result def _check_for_lua_globals(self, lua_runtime, expected_globals): actual_globals = set(lua_runtime.globals().keys()) if actual_globals != expected_globals: unexpected = [six.ensure_str(var, 'utf-8', 'replace') for var in actual_globals - expected_globals] raise redis.ResponseError(GLOBAL_VARIABLE_MSG.format(", ".join(unexpected))) def _lua_redis_call(self, lua_runtime, expected_globals, op, *args): # Check if we've set any global variables before making any change. self._check_for_lua_globals(lua_runtime, expected_globals) func, func_name = self._name_to_func(op) args = [self._convert_redis_arg(lua_runtime, arg) for arg in args] result = self._run_command(func, func._fakeredis_sig, args, True) return self._convert_redis_result(lua_runtime, result) def _lua_redis_pcall(self, lua_runtime, expected_globals, op, *args): try: return self._lua_redis_call(lua_runtime, expected_globals, op, *args) except Exception as ex: return lua_runtime.table_from({b"err": str(ex)}) @command((bytes, Int), (bytes,), flags='s') def eval(self, script, numkeys, *keys_and_args): from lupa import LuaRuntime, LuaError if numkeys > len(keys_and_args): raise redis.ResponseError(TOO_MANY_KEYS_MSG) if numkeys < 0: raise redis.ResponseError(NEGATIVE_KEYS_MSG) sha1 = hashlib.sha1(script).hexdigest().encode() self._server.script_cache[sha1] = script lua_runtime = LuaRuntime(encoding=None, unpack_returned_tuples=True) set_globals = lua_runtime.eval( """ function(keys, argv, redis_call, redis_pcall) redis = {} redis.call = redis_call redis.pcall = redis_pcall redis.error_reply = function(msg) return {err=msg} end redis.status_reply = function(msg) return {ok=msg} end KEYS = keys ARGV = argv end """ ) expected_globals = set() set_globals( lua_runtime.table_from(keys_and_args[:numkeys]), lua_runtime.table_from(keys_and_args[numkeys:]), functools.partial(self._lua_redis_call, lua_runtime, expected_globals), functools.partial(self._lua_redis_pcall, lua_runtime, expected_globals) ) expected_globals.update(lua_runtime.globals().keys()) try: result = lua_runtime.execute(script) except LuaError as ex: raise redis.ResponseError(str(ex)) self._check_for_lua_globals(lua_runtime, expected_globals) return self._convert_lua_result(result, nested=False) @command((bytes, Int), (bytes,), flags='s') def evalsha(self, sha1, numkeys, *keys_and_args): try: script = self._server.script_cache[sha1] except KeyError: raise redis.exceptions.NoScriptError(NO_MATCHING_SCRIPT_MSG) return self.eval(script, numkeys, *keys_and_args) @command((bytes,), (bytes,), flags='s') def script(self, subcmd, *args): if casematch(subcmd, b'load'): if len(args) != 1: raise redis.ResponseError(BAD_SUBCOMMAND_MSG.format('SCRIPT')) script = args[0] sha1 = hashlib.sha1(script).hexdigest().encode() self._server.script_cache[sha1] = script return sha1 else: raise redis.ResponseError(BAD_SUBCOMMAND_MSG.format('SCRIPT')) # Pubsub commands # TODO: pubsub command def _subscribe(self, channels, subscribers, mtype): for channel in channels: subs = subscribers[channel] if self not in subs: subs.add(self) self._pubsub += 1 msg = [mtype, channel, self._pubsub] self.responses.put(msg) return NoResponse() def _unsubscribe(self, channels, subscribers, mtype): if not channels: channels = [] for (channel, subs) in subscribers.items(): if self in subs: channels.append(channel) for channel in channels: subs = subscribers.get(channel, set()) if self in subs: subs.remove(self) if not subs: del subscribers[channel] self._pubsub -= 1 msg = [mtype, channel, self._pubsub] self.responses.put(msg) return NoResponse() @command((bytes,), (bytes,), flags='s') def psubscribe(self, *patterns): return self._subscribe(patterns, self._server.psubscribers, b'psubscribe') @command((bytes,), (bytes,), flags='s') def subscribe(self, *channels): return self._subscribe(channels, self._server.subscribers, b'subscribe') @command((), (bytes,), flags='s') def punsubscribe(self, *patterns): return self._unsubscribe(patterns, self._server.psubscribers, b'punsubscribe') @command((), (bytes,), flags='s') def unsubscribe(self, *channels): return self._unsubscribe(channels, self._server.subscribers, b'unsubscribe') @command((bytes, bytes)) def publish(self, channel, message): receivers = 0 msg = [b'message', channel, message] subs = self._server.subscribers.get(channel, set()) for sock in subs: sock.responses.put(msg) receivers += 1 for (pattern, socks) in self._server.psubscribers.items(): regex = compile_pattern(pattern) if regex.match(channel): msg = [b'pmessage', pattern, channel, message] for sock in socks: sock.responses.put(msg) receivers += 1 return receivers setattr(FakeSocket, 'del', FakeSocket.del_) delattr(FakeSocket, 'del_') setattr(FakeSocket, 'set', FakeSocket.set_) delattr(FakeSocket, 'set_') setattr(FakeSocket, 'exec', FakeSocket.exec_) delattr(FakeSocket, 'exec_') class _DummyParser: def __init__(self, socket_read_size): self.socket_read_size = socket_read_size def on_disconnect(self): pass def on_connect(self, connection): pass # Redis <3.2 will not have a selector try: from redis.selector import BaseSelector except ImportError: class BaseSelector: def __init__(self, sock): self.sock = sock class FakeSelector(BaseSelector): def check_can_read(self, timeout): if self.sock.responses.qsize(): return True if timeout <= 0: return False # A sleep/poll loop is easier to mock out than messing with condition # variables. start = time.time() while True: if self.sock.responses.qsize(): return True time.sleep(0.01) now = time.time() if now > start + timeout: return False def check_is_ready_for_command(self, timeout): return True class FakeConnection(redis.Connection): description_format = "FakeConnection" def __init__(self, server, db=0, username=None, password=None, socket_timeout=None, socket_connect_timeout=None, socket_keepalive=False, socket_keepalive_options=None, socket_type=0, retry_on_timeout=False, encoding='utf-8', encoding_errors='strict', decode_responses=False, parser_class=_DummyParser, socket_read_size=65536, health_check_interval=0, client_name=None): self.pid = os.getpid() self.db = db self.username = username self.client_name = client_name self.password = password # Allow socket attributes to be passed in and saved even if they aren't used self.socket_timeout = socket_timeout self.socket_connect_timeout = socket_connect_timeout or socket_timeout self.socket_keepalive = socket_keepalive self.socket_keepalive_options = socket_keepalive_options or {} self.socket_type = socket_type self.retry_on_timeout = retry_on_timeout self.encoder = redis.connection.Encoder(encoding, encoding_errors, decode_responses) self._description_args = {'db': self.db} self._connect_callbacks = [] self._buffer_cutoff = 6000 self._server = server # self._parser isn't used for anything, but some of the # base class methods depend on it and it's easier not to # override them. self._parser = parser_class(socket_read_size=socket_read_size) self._sock = None # added in redis==3.3.0 self.health_check_interval = health_check_interval self.next_health_check = 0 def connect(self): super().connect() # The selector is set in redis.Connection.connect() after _connect() is called self._selector = FakeSelector(self._sock) def _connect(self): if not self._server.connected: raise redis.ConnectionError(CONNECTION_ERROR_MSG) return FakeSocket(self._server) def can_read(self, timeout=0): if not self._server.connected: return True if not self._sock: self.connect() # We use check_can_read rather than can_read, because on redis-py<3.2, # FakeSelector inherits from a stub BaseSelector which doesn't # implement can_read. Normally can_read provides retries on EINTR, # but that's not necessary for the implementation of # FakeSelector.check_can_read. return self._selector.check_can_read(timeout) def _decode(self, response): if isinstance(response, list): return [self._decode(item) for item in response] elif isinstance(response, bytes): return self.encoder.decode(response) else: return response def read_response(self): if not self._server.connected: try: response = self._sock.responses.get_nowait() except queue.Empty: raise redis.ConnectionError(CONNECTION_ERROR_MSG) else: response = self._sock.responses.get() if isinstance(response, redis.ResponseError): raise response return self._decode(response) def repr_pieces(self): pieces = [ ('server', self._server), ('db', self.db) ] if self.client_name: pieces.append(('client_name', self.client_name)) return pieces class FakeRedisMixin: def __init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, socket_connect_timeout=None, socket_keepalive=None, socket_keepalive_options=None, connection_pool=None, unix_socket_path=None, encoding='utf-8', encoding_errors='strict', charset=None, errors=None, decode_responses=False, retry_on_timeout=False, ssl=False, ssl_keyfile=None, ssl_certfile=None, ssl_cert_reqs=None, ssl_ca_certs=None, max_connections=None, server=None, connected=True): if not connection_pool: # Adapted from redis-py if charset is not None: warnings.warn(DeprecationWarning( '"charset" is deprecated. Use "encoding" instead')) encoding = charset if errors is not None: warnings.warn(DeprecationWarning( '"errors" is deprecated. Use "encoding_errors" instead')) encoding_errors = errors if server is None: server = FakeServer() server.connected = connected kwargs = { 'db': db, 'password': password, 'encoding': encoding, 'encoding_errors': encoding_errors, 'decode_responses': decode_responses, 'max_connections': max_connections, 'connection_class': FakeConnection, 'server': server } connection_pool = redis.connection.ConnectionPool(**kwargs) # These need to be passed by name due to # https://github.com/andymccurdy/redis-py/issues/1276 super().__init__( host=host, port=port, db=db, password=password, socket_timeout=socket_timeout, socket_connect_timeout=socket_connect_timeout, socket_keepalive=socket_keepalive, socket_keepalive_options=socket_keepalive_options, connection_pool=connection_pool, unix_socket_path=unix_socket_path, encoding=encoding, encoding_errors=encoding_errors, charset=charset, errors=errors, decode_responses=decode_responses, retry_on_timeout=retry_on_timeout, ssl=ssl, ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile, ssl_cert_reqs=ssl_cert_reqs, ssl_ca_certs=ssl_ca_certs, max_connections=max_connections) @classmethod def from_url(cls, url, db=None, **kwargs): server = kwargs.pop('server', None) if server is None: server = FakeServer() self = super().from_url(url, db, **kwargs) # Now override how it creates connections pool = self.connection_pool pool.connection_class = FakeConnection pool.connection_kwargs['server'] = server for key in ['password', 'host', 'port', 'path']: if key in pool.connection_kwargs: del pool.connection_kwargs[key] return self class FakeStrictRedis(FakeRedisMixin, redis.StrictRedis): pass class FakeRedis(FakeRedisMixin, redis.Redis): pass fakeredis-1.2.1/fakeredis/_zset.py000066400000000000000000000052051362144536200171130ustar00rootroot00000000000000import sortedcontainers class ZSet: def __init__(self): self._bylex = {} # Maps value to score self._byscore = sortedcontainers.SortedList() def __contains__(self, value): return value in self._bylex def add(self, value, score): """Update the item and return whether it modified the zset""" old_score = self._bylex.get(value, None) if old_score is not None: if score == old_score: return False self._byscore.remove((old_score, value)) self._bylex[value] = score self._byscore.add((score, value)) return True def __setitem__(self, value, score): self.add(value, score) def __getitem__(self, key): return self._bylex[key] def get(self, key, default=None): return self._bylex.get(key, default) def __len__(self): return len(self._bylex) def __iter__(self): def gen(): for score, value in self._byscore: yield value return gen() def discard(self, key): try: score = self._bylex.pop(key) except KeyError: return else: self._byscore.remove((score, key)) def zcount(self, min_, max_): pos1 = self._byscore.bisect_left(min_) pos2 = self._byscore.bisect_left(max_) return max(0, pos2 - pos1) def zlexcount(self, min_value, min_exclusive, max_value, max_exclusive): if not self._byscore: return 0 score = self._byscore[0][0] if min_exclusive: pos1 = self._byscore.bisect_right((score, min_value)) else: pos1 = self._byscore.bisect_left((score, min_value)) if max_exclusive: pos2 = self._byscore.bisect_left((score, max_value)) else: pos2 = self._byscore.bisect_right((score, max_value)) return max(0, pos2 - pos1) def islice_score(self, start, stop, reverse=False): return self._byscore.islice(start, stop, reverse) def irange_lex(self, start, stop, inclusive=(True, True), reverse=False): if not self._byscore: return iter([]) score = self._byscore[0][0] it = self._byscore.irange((score, start), (score, stop), inclusive=inclusive, reverse=reverse) return (item[1] for item in it) def irange_score(self, start, stop, reverse=False): return self._byscore.irange(start, stop, reverse=reverse) def rank(self, member): return self._byscore.index((self._bylex[member], member)) def items(self): return self._bylex.items() fakeredis-1.2.1/requirements-dev.txt000066400000000000000000000000661362144536200175200ustar00rootroot00000000000000invoke==0.22.1 wheel==0.31.1 tox==3.6.1 twine==1.12.1 fakeredis-1.2.1/requirements.in000066400000000000000000000003061362144536200165300ustar00rootroot00000000000000coverage flake8 hypothesis lupa pytest pytest-cov redis==3.4.1 # Latest at time of writing six sortedcontainers # Not needed directly, but the latest versions don't support Python 3.5 zipp<2 fakeredis-1.2.1/requirements.txt000066400000000000000000000013501362144536200167410ustar00rootroot00000000000000# # This file is autogenerated by pip-compile # To update, run: # # pip-compile requirements.in # attrs==19.3.0 # via hypothesis, pytest coverage==5.0.3 entrypoints==0.3 # via flake8 flake8==3.7.9 hypothesis==5.4.1 importlib-metadata==1.5.0 # via pluggy, pytest lupa==1.9 mccabe==0.6.1 # via flake8 more-itertools==8.2.0 # via pytest packaging==20.1 # via pytest pluggy==0.13.1 # via pytest py==1.8.1 # via pytest pycodestyle==2.5.0 # via flake8 pyflakes==2.1.1 # via flake8 pyparsing==2.4.6 # via packaging pytest-cov==2.8.1 pytest==5.3.5 redis==3.4.1 six==1.14.0 sortedcontainers==2.1.0 wcwidth==0.1.8 # via pytest zipp==1.1.0 fakeredis-1.2.1/scripts/000077500000000000000000000000001362144536200151455ustar00rootroot00000000000000fakeredis-1.2.1/scripts/supported000077500000000000000000000034451362144536200171260ustar00rootroot00000000000000#!/usr/bin/env python # Script will import fakeredis and list what # commands it does not have support for, based on the # command list from: # https://raw.github.com/antirez/redis-doc/master/commands.json # Because, who wants to do this by hand... from __future__ import print_function import os import json import inspect from collections import OrderedDict import requests import fakeredis THIS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__))) COMMANDS_FILE = os.path.join(THIS_DIR, '.commands.json') COMMANDS_URL = 'https://raw.github.com/antirez/redis-doc/master/commands.json' if not os.path.exists(COMMANDS_FILE): contents = requests.get(COMMANDS_URL).content open(COMMANDS_FILE, 'wb').write(contents) commands = json.load(open(COMMANDS_FILE), object_pairs_hook=OrderedDict) for k, v in list(commands.items()): commands[k.lower()] = v del commands[k] implemented_commands = set() for name, method in inspect.getmembers(fakeredis.server.FakeSocket): if hasattr(method, '_fakeredis_sig'): implemented_commands.add(name) # Currently no programmatic way to discover implemented subcommands implemented_commands.add('script load') unimplemented_commands = [] for command in commands: if command not in implemented_commands: unimplemented_commands.append(command) # Group by 'group' for easier to read output groups = OrderedDict() for command in unimplemented_commands: group = commands[command]['group'] groups.setdefault(group, []).append(command) print(""" Unimplemented Commands ====================== All of the redis commands are implemented in fakeredis with these exceptions: """) for group in groups: print(group) print("-" * len(str(group))) print() for command in groups[group]: print(" *", command) print("\n") fakeredis-1.2.1/setup.cfg000066400000000000000000000001661362144536200153020ustar00rootroot00000000000000[flake8] max-line-length = 119 [tool:pytest] markers = slow: marks tests as slow (deselect with '-m "not slow"') fakeredis-1.2.1/setup.py000066400000000000000000000024021362144536200151660ustar00rootroot00000000000000import os from setuptools import setup setup( name='fakeredis', version='1.2.1', description="Fake implementation of redis API for testing purposes.", long_description=open(os.path.join(os.path.dirname(__file__), 'README.rst')).read(), license='BSD', url="https://github.com/jamesls/fakeredis", author='James Saryerwinnie', author_email='js@jamesls.com', maintainer='Bruce Merry', maintainer_email='bmerry@ska.ac.za', packages=['fakeredis'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8' ], python_requires='>=3.5', install_requires=[ # Minor version updates to redis tend to break fakeredis. If you # need to use fakeredis with a newer redis, please submit a PR that # relaxes this restriction and adds it to the Travis tests. 'redis<3.5', 'six>=1.12', 'sortedcontainers' ], extras_require={ "lua": ['lupa'] } ) fakeredis-1.2.1/test_fakeredis.py000066400000000000000000005644411362144536200170420ustar00rootroot00000000000000#!/usr/bin/env python from collections import namedtuple from time import sleep, time from redis.exceptions import ResponseError import inspect from functools import wraps from collections import OrderedDict import os import sys import math import threading import unittest from queue import Queue import distutils.version import six import pytest import redis import redis.client import fakeredis from datetime import datetime, timedelta REDIS_VERSION = distutils.version.LooseVersion(redis.__version__) REDIS3 = REDIS_VERSION >= '3' UpdateCommand = namedtuple('UpdateCommand', 'input expected_return_value expected_state') def redis_must_be_running(cls): # This can probably be improved. This will determines # at import time if the tests should be run, but we probably # want it to be when the tests are actually run. try: r = redis.StrictRedis('localhost', port=6379) r.ping() except redis.ConnectionError: redis_running = False else: redis_running = True if not redis_running: for name, attribute in inspect.getmembers(cls): if name.startswith('test_'): @wraps(attribute) def skip_test(*args, **kwargs): pytest.skip("Redis is not running.") setattr(cls, name, skip_test) cls.setUp = lambda x: None cls.tearDown = lambda x: None return cls redis2_only = pytest.mark.skipif(REDIS3, reason="Test is only applicable to redis-py 2.x") redis3_only = pytest.mark.skipif(not REDIS3, reason="Test is only applicable to redis-py 3.x") def key_val_dict(size=100): return {b'key:' + bytes([i]): b'val:' + bytes([i]) for i in range(size)} class TestFakeStrictRedis(unittest.TestCase): decode_responses = False def setUp(self): self.server = fakeredis.FakeServer() self.redis = self.create_redis() def tearDown(self): self.redis.flushall() del self.redis if sys.version_info >= (3,): def assertItemsEqual(self, a, b): return self.assertCountEqual(a, b) def create_redis(self, db=0): return fakeredis.FakeStrictRedis(db=db, server=self.server) def _round_str(self, x): self.assertIsInstance(x, bytes) return round(float(x)) def raw_command(self, *args): """Like execute_command, but does not do command-specific response parsing""" response_callbacks = self.redis.response_callbacks try: self.redis.response_callbacks = {} return self.redis.execute_command(*args) finally: self.redis.response_callbacks = response_callbacks # Wrap some redis commands to abstract differences between redis-py 2 and 3. def zadd(self, key, d, *args, **kwargs): if REDIS3: return self.redis.zadd(key, d, *args, **kwargs) else: return self.redis.zadd(key, **d) def zincrby(self, key, amount, value): if REDIS3: return self.redis.zincrby(key, amount, value) else: return self.redis.zincrby(key, value, amount) def test_large_command(self): self.redis.set('foo', 'bar' * 10000) self.assertEqual(self.redis.get('foo'), b'bar' * 10000) def test_dbsize(self): self.assertEqual(self.redis.dbsize(), 0) self.redis.set('foo', 'bar') self.redis.set('bar', 'foo') self.assertEqual(self.redis.dbsize(), 2) def test_flushdb(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.keys(), [b'foo']) self.assertEqual(self.redis.flushdb(), True) self.assertEqual(self.redis.keys(), []) def test_set_then_get(self): self.assertEqual(self.redis.set('foo', 'bar'), True) self.assertEqual(self.redis.get('foo'), b'bar') @redis2_only def test_set_None_value(self): self.assertEqual(self.redis.set('foo', None), True) self.assertEqual(self.redis.get('foo'), b'None') def test_set_float_value(self): x = 1.23456789123456789 self.redis.set('foo', x) self.assertEqual(float(self.redis.get('foo')), x) def test_saving_non_ascii_chars_as_value(self): self.assertEqual(self.redis.set('foo', 'Ñandu'), True) self.assertEqual(self.redis.get('foo'), 'Ñandu'.encode()) def test_saving_unicode_type_as_value(self): self.assertEqual(self.redis.set('foo', 'Ñandu'), True) self.assertEqual(self.redis.get('foo'), 'Ñandu'.encode()) def test_saving_non_ascii_chars_as_key(self): self.assertEqual(self.redis.set('Ñandu', 'foo'), True) self.assertEqual(self.redis.get('Ñandu'), b'foo') def test_saving_unicode_type_as_key(self): self.assertEqual(self.redis.set('Ñandu', 'foo'), True) self.assertEqual(self.redis.get('Ñandu'), b'foo') def test_future_newbytes(self): bytes = pytest.importorskip('builtins', reason='future.types not available').bytes self.redis.set(bytes(b'\xc3\x91andu'), 'foo') self.assertEqual(self.redis.get('Ñandu'), b'foo') def test_future_newstr(self): str = pytest.importorskip('builtins', reason='future.types not available').str self.redis.set(str('Ñandu'), 'foo') self.assertEqual(self.redis.get('Ñandu'), b'foo') def test_get_does_not_exist(self): self.assertEqual(self.redis.get('foo'), None) def test_get_with_non_str_keys(self): self.assertEqual(self.redis.set('2', 'bar'), True) self.assertEqual(self.redis.get(2), b'bar') def test_get_invalid_type(self): self.assertEqual(self.redis.hset('foo', 'key', 'value'), 1) with self.assertRaises(redis.ResponseError): self.redis.get('foo') def test_set_non_str_keys(self): self.assertEqual(self.redis.set(2, 'bar'), True) self.assertEqual(self.redis.get(2), b'bar') self.assertEqual(self.redis.get('2'), b'bar') def test_getbit(self): self.redis.setbit('foo', 3, 1) self.assertEqual(self.redis.getbit('foo', 0), 0) self.assertEqual(self.redis.getbit('foo', 1), 0) self.assertEqual(self.redis.getbit('foo', 2), 0) self.assertEqual(self.redis.getbit('foo', 3), 1) self.assertEqual(self.redis.getbit('foo', 4), 0) self.assertEqual(self.redis.getbit('foo', 100), 0) def test_getbit_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.getbit('foo', 1) def test_multiple_bits_set(self): self.redis.setbit('foo', 1, 1) self.redis.setbit('foo', 3, 1) self.redis.setbit('foo', 5, 1) self.assertEqual(self.redis.getbit('foo', 0), 0) self.assertEqual(self.redis.getbit('foo', 1), 1) self.assertEqual(self.redis.getbit('foo', 2), 0) self.assertEqual(self.redis.getbit('foo', 3), 1) self.assertEqual(self.redis.getbit('foo', 4), 0) self.assertEqual(self.redis.getbit('foo', 5), 1) self.assertEqual(self.redis.getbit('foo', 6), 0) def test_unset_bits(self): self.redis.setbit('foo', 1, 1) self.redis.setbit('foo', 2, 0) self.redis.setbit('foo', 3, 1) self.assertEqual(self.redis.getbit('foo', 1), 1) self.redis.setbit('foo', 1, 0) self.assertEqual(self.redis.getbit('foo', 1), 0) self.redis.setbit('foo', 3, 0) self.assertEqual(self.redis.getbit('foo', 3), 0) def test_get_set_bits(self): # set bit 5 self.assertFalse(self.redis.setbit('a', 5, True)) self.assertTrue(self.redis.getbit('a', 5)) # unset bit 4 self.assertFalse(self.redis.setbit('a', 4, False)) self.assertFalse(self.redis.getbit('a', 4)) # set bit 4 self.assertFalse(self.redis.setbit('a', 4, True)) self.assertTrue(self.redis.getbit('a', 4)) # set bit 5 again self.assertTrue(self.redis.setbit('a', 5, True)) self.assertTrue(self.redis.getbit('a', 5)) def test_setbits_and_getkeys(self): # The bit operations and the get commands # should play nicely with each other. self.redis.setbit('foo', 1, 1) self.assertEqual(self.redis.get('foo'), b'@') self.redis.setbit('foo', 2, 1) self.assertEqual(self.redis.get('foo'), b'`') self.redis.setbit('foo', 3, 1) self.assertEqual(self.redis.get('foo'), b'p') self.redis.setbit('foo', 9, 1) self.assertEqual(self.redis.get('foo'), b'p@') self.redis.setbit('foo', 54, 1) self.assertEqual(self.redis.get('foo'), b'p@\x00\x00\x00\x00\x02') def test_setbit_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.setbit('foo', 0, 1) def test_setbit_expiry(self): self.redis.set('foo', b'0x00', ex=10) self.redis.setbit('foo', 1, 1) self.assertGreater(self.redis.ttl('foo'), 0) def test_bitcount(self): self.redis.delete('foo') self.assertEqual(self.redis.bitcount('foo'), 0) self.redis.setbit('foo', 1, 1) self.assertEqual(self.redis.bitcount('foo'), 1) self.redis.setbit('foo', 8, 1) self.assertEqual(self.redis.bitcount('foo'), 2) self.assertEqual(self.redis.bitcount('foo', 1, 1), 1) self.redis.setbit('foo', 57, 1) self.assertEqual(self.redis.bitcount('foo'), 3) self.redis.set('foo', ' ') self.assertEqual(self.redis.bitcount('foo'), 1) def test_bitcount_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.bitcount('foo') def test_getset_not_exist(self): val = self.redis.getset('foo', 'bar') self.assertEqual(val, None) self.assertEqual(self.redis.get('foo'), b'bar') def test_getset_exists(self): self.redis.set('foo', 'bar') val = self.redis.getset('foo', b'baz') self.assertEqual(val, b'bar') val = self.redis.getset('foo', b'baz2') self.assertEqual(val, b'baz') def test_getset_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.getset('foo', 'bar') def test_setitem_getitem(self): self.assertEqual(self.redis.keys(), []) self.redis['foo'] = 'bar' self.assertEqual(self.redis['foo'], b'bar') def test_getitem_non_existent_key(self): self.assertEqual(self.redis.keys(), []) with self.assertRaises(KeyError): self.redis['noexists'] def test_strlen(self): self.redis['foo'] = 'bar' self.assertEqual(self.redis.strlen('foo'), 3) self.assertEqual(self.redis.strlen('noexists'), 0) def test_strlen_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.strlen('foo') def test_substr(self): self.redis['foo'] = 'one_two_three' self.assertEqual(self.redis.substr('foo', 0), b'one_two_three') self.assertEqual(self.redis.substr('foo', 0, 2), b'one') self.assertEqual(self.redis.substr('foo', 4, 6), b'two') self.assertEqual(self.redis.substr('foo', -5), b'three') self.assertEqual(self.redis.substr('foo', -4, -5), b'') self.assertEqual(self.redis.substr('foo', -5, -3), b'thr') def test_substr_noexist_key(self): self.assertEqual(self.redis.substr('foo', 0), b'') self.assertEqual(self.redis.substr('foo', 10), b'') self.assertEqual(self.redis.substr('foo', -5, -1), b'') def test_substr_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.substr('foo', 0) def test_append(self): self.assertTrue(self.redis.set('foo', 'bar')) self.assertEqual(self.redis.append('foo', 'baz'), 6) self.assertEqual(self.redis.get('foo'), b'barbaz') def test_append_with_no_preexisting_key(self): self.assertEqual(self.redis.append('foo', 'bar'), 3) self.assertEqual(self.redis.get('foo'), b'bar') def test_append_wrong_type(self): self.redis.rpush('foo', b'x') with self.assertRaises(redis.ResponseError): self.redis.append('foo', b'x') def test_incr_with_no_preexisting_key(self): self.assertEqual(self.redis.incr('foo'), 1) self.assertEqual(self.redis.incr('bar', 2), 2) def test_incr_by(self): self.assertEqual(self.redis.incrby('foo'), 1) self.assertEqual(self.redis.incrby('bar', 2), 2) def test_incr_preexisting_key(self): self.redis.set('foo', 15) self.assertEqual(self.redis.incr('foo', 5), 20) self.assertEqual(self.redis.get('foo'), b'20') def test_incr_expiry(self): self.redis.set('foo', 15, ex=10) self.redis.incr('foo', 5) self.assertGreater(self.redis.ttl('foo'), 0) def test_incr_bad_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.incr('foo', 15) self.redis.rpush('foo2', 1) with self.assertRaises(redis.ResponseError): self.redis.incr('foo2', 15) def test_incr_with_float(self): with self.assertRaises(redis.ResponseError): self.redis.incr('foo', 2.0) def test_incr_followed_by_mget(self): self.redis.set('foo', 15) self.assertEqual(self.redis.incr('foo', 5), 20) self.assertEqual(self.redis.get('foo'), b'20') def test_incr_followed_by_mget_returns_strings(self): self.redis.incr('foo', 1) self.assertEqual(self.redis.mget(['foo']), [b'1']) def test_incrbyfloat(self): self.redis.set('foo', 0) self.assertEqual(self.redis.incrbyfloat('foo', 1.0), 1.0) self.assertEqual(self.redis.incrbyfloat('foo', 1.0), 2.0) def test_incrbyfloat_with_noexist(self): self.assertEqual(self.redis.incrbyfloat('foo', 1.0), 1.0) self.assertEqual(self.redis.incrbyfloat('foo', 1.0), 2.0) def test_incrbyfloat_expiry(self): self.redis.set('foo', 1.5, ex=10) self.redis.incrbyfloat('foo', 2.5) self.assertGreater(self.redis.ttl('foo'), 0) def test_incrbyfloat_bad_type(self): self.redis.set('foo', 'bar') with self.assertRaisesRegex(redis.ResponseError, 'not a valid float'): self.redis.incrbyfloat('foo', 1.0) self.redis.rpush('foo2', 1) with self.assertRaises(redis.ResponseError): self.redis.incrbyfloat('foo2', 1.0) def test_incrbyfloat_precision(self): x = 1.23456789123456789 self.assertEqual(self.redis.incrbyfloat('foo', x), x) self.assertEqual(float(self.redis.get('foo')), x) def test_decr(self): self.redis.set('foo', 10) self.assertEqual(self.redis.decr('foo'), 9) self.assertEqual(self.redis.get('foo'), b'9') def test_decr_newkey(self): self.redis.decr('foo') self.assertEqual(self.redis.get('foo'), b'-1') def test_decr_expiry(self): self.redis.set('foo', 10, ex=10) self.redis.decr('foo', 5) self.assertGreater(self.redis.ttl('foo'), 0) def test_decr_badtype(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.decr('foo', 15) self.redis.rpush('foo2', 1) with self.assertRaises(redis.ResponseError): self.redis.decr('foo2', 15) def test_keys(self): self.redis.set('', 'empty') self.redis.set('abc\n', '') self.redis.set('abc\\', '') self.redis.set('abcde', '') if self.decode_responses: self.assertEqual(sorted(self.redis.keys()), [b'', b'abc\n', b'abc\\', b'abcde']) else: self.redis.set(b'\xfe\xcd', '') self.assertEqual(sorted(self.redis.keys()), [b'', b'abc\n', b'abc\\', b'abcde', b'\xfe\xcd']) self.assertEqual(self.redis.keys('??'), [b'\xfe\xcd']) # empty pattern not the same as no pattern self.assertEqual(self.redis.keys(''), [b'']) # ? must match \n self.assertEqual(sorted(self.redis.keys('abc?')), [b'abc\n', b'abc\\']) # must be anchored at both ends self.assertEqual(self.redis.keys('abc'), []) self.assertEqual(self.redis.keys('bcd'), []) # wildcard test self.assertEqual(self.redis.keys('a*de'), [b'abcde']) # positive groups self.assertEqual(sorted(self.redis.keys('abc[d\n]*')), [b'abc\n', b'abcde']) self.assertEqual(self.redis.keys('abc[c-e]?'), [b'abcde']) self.assertEqual(self.redis.keys('abc[e-c]?'), [b'abcde']) self.assertEqual(self.redis.keys('abc[e-e]?'), []) self.assertEqual(self.redis.keys('abcd[ef'), [b'abcde']) self.assertEqual(self.redis.keys('abcd[]'), []) # negative groups self.assertEqual(self.redis.keys('abc[^d\\\\]*'), [b'abc\n']) self.assertEqual(self.redis.keys('abc[^]e'), [b'abcde']) # escaping self.assertEqual(self.redis.keys(r'abc\?e'), []) self.assertEqual(self.redis.keys(r'abc\de'), [b'abcde']) self.assertEqual(self.redis.keys(r'abc[\d]e'), [b'abcde']) # some escaping cases that redis handles strangely self.assertEqual(self.redis.keys('abc\\'), [b'abc\\']) self.assertEqual(self.redis.keys(r'abc[\c-e]e'), []) self.assertEqual(self.redis.keys(r'abc[c-\e]e'), []) def test_exists(self): self.assertFalse('foo' in self.redis) self.redis.set('foo', 'bar') self.assertTrue('foo' in self.redis) def test_contains(self): self.assertFalse(self.redis.exists('foo')) self.redis.set('foo', 'bar') self.assertTrue(self.redis.exists('foo')) def test_rename(self): self.redis.set('foo', 'unique value') self.assertTrue(self.redis.rename('foo', 'bar')) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.get('bar'), b'unique value') def test_rename_nonexistent_key(self): with self.assertRaises(redis.ResponseError): self.redis.rename('foo', 'bar') def test_renamenx_doesnt_exist(self): self.redis.set('foo', 'unique value') self.assertTrue(self.redis.renamenx('foo', 'bar')) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.get('bar'), b'unique value') def test_rename_does_exist(self): self.redis.set('foo', 'unique value') self.redis.set('bar', 'unique value2') self.assertFalse(self.redis.renamenx('foo', 'bar')) self.assertEqual(self.redis.get('foo'), b'unique value') self.assertEqual(self.redis.get('bar'), b'unique value2') def test_rename_expiry(self): self.redis.set('foo', 'value1', ex=10) self.redis.set('bar', 'value2') self.redis.rename('foo', 'bar') self.assertGreater(self.redis.ttl('bar'), 0) def test_mget(self): self.redis.set('foo', 'one') self.redis.set('bar', 'two') self.assertEqual(self.redis.mget(['foo', 'bar']), [b'one', b'two']) self.assertEqual(self.redis.mget(['foo', 'bar', 'baz']), [b'one', b'two', None]) self.assertEqual(self.redis.mget('foo', 'bar'), [b'one', b'two']) @redis2_only def test_mget_none(self): self.redis.set('foo', 'one') self.redis.set('bar', 'two') self.assertEqual(self.redis.mget('foo', 'bar', None), [b'one', b'two', None]) def test_mget_with_no_keys(self): if REDIS3: self.assertEqual(self.redis.mget([]), []) else: with self.assertRaisesRegex( redis.ResponseError, 'wrong number of arguments'): self.redis.mget([]) def test_mget_mixed_types(self): self.redis.hset('hash', 'bar', 'baz') self.zadd('zset', {'bar': 1}) self.redis.sadd('set', 'member') self.redis.rpush('list', 'item1') self.redis.set('string', 'value') self.assertEqual( self.redis.mget(['hash', 'zset', 'set', 'string', 'absent']), [None, None, None, b'value', None]) def test_mset_with_no_keys(self): with self.assertRaises(redis.ResponseError): self.redis.mset({}) def test_mset(self): self.assertEqual(self.redis.mset({'foo': 'one', 'bar': 'two'}), True) self.assertEqual(self.redis.mset({'foo': 'one', 'bar': 'two'}), True) self.assertEqual(self.redis.mget('foo', 'bar'), [b'one', b'two']) @redis2_only def test_mset_accepts_kwargs(self): self.assertEqual( self.redis.mset(foo='one', bar='two'), True) self.assertEqual( self.redis.mset(foo='one', baz='three'), True) self.assertEqual(self.redis.mget('foo', 'bar', 'baz'), [b'one', b'two', b'three']) def test_msetnx(self): self.assertEqual(self.redis.msetnx({'foo': 'one', 'bar': 'two'}), True) self.assertEqual(self.redis.msetnx({'bar': 'two', 'baz': 'three'}), False) self.assertEqual(self.redis.mget('foo', 'bar', 'baz'), [b'one', b'two', None]) def test_setex(self): self.assertEqual(self.redis.setex('foo', 100, 'bar'), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_setex_using_timedelta(self): self.assertEqual( self.redis.setex('foo', timedelta(seconds=100), 'bar'), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_setex_using_float(self): self.assertRaisesRegex( redis.ResponseError, 'integer', self.redis.setex, 'foo', 1.2, 'bar') def test_set_ex(self): self.assertEqual(self.redis.set('foo', 'bar', ex=100), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_set_ex_using_timedelta(self): self.assertEqual( self.redis.set('foo', 'bar', ex=timedelta(seconds=100)), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_set_px(self): self.assertEqual(self.redis.set('foo', 'bar', px=100), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_set_px_using_timedelta(self): self.assertEqual( self.redis.set('foo', 'bar', px=timedelta(milliseconds=100)), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_set_raises_wrong_ex(self): with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', ex=-100) with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', ex=0) self.assertFalse(self.redis.exists('foo')) def test_set_using_timedelta_raises_wrong_ex(self): with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', ex=timedelta(seconds=-100)) with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', ex=timedelta(seconds=0)) self.assertFalse(self.redis.exists('foo')) def test_set_raises_wrong_px(self): with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', px=-100) with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', px=0) self.assertFalse(self.redis.exists('foo')) def test_set_using_timedelta_raises_wrong_px(self): with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', px=timedelta(milliseconds=-100)) with self.assertRaises(ResponseError): self.redis.set('foo', 'bar', px=timedelta(milliseconds=0)) self.assertFalse(self.redis.exists('foo')) def test_setex_raises_wrong_ex(self): with self.assertRaises(ResponseError): self.redis.setex('foo', -100, 'bar') with self.assertRaises(ResponseError): self.redis.setex('foo', 0, 'bar') self.assertFalse(self.redis.exists('foo')) def test_setex_using_timedelta_raises_wrong_ex(self): with self.assertRaises(ResponseError): self.redis.setex('foo', timedelta(seconds=-100), 'bar') with self.assertRaises(ResponseError): self.redis.setex('foo', timedelta(seconds=-100), 'bar') self.assertFalse(self.redis.exists('foo')) def test_setnx(self): self.assertEqual(self.redis.setnx('foo', 'bar'), True) self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.setnx('foo', 'baz'), False) self.assertEqual(self.redis.get('foo'), b'bar') def test_set_nx(self): self.assertEqual(self.redis.set('foo', 'bar', nx=True), True) self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.set('foo', 'bar', nx=True), None) self.assertEqual(self.redis.get('foo'), b'bar') def test_set_xx(self): self.assertEqual(self.redis.set('foo', 'bar', xx=True), None) self.redis.set('foo', 'bar') self.assertEqual(self.redis.set('foo', 'bar', xx=True), True) def test_del_operator(self): self.redis['foo'] = 'bar' del self.redis['foo'] self.assertEqual(self.redis.get('foo'), None) def test_delete(self): self.redis['foo'] = 'bar' self.assertEqual(self.redis.delete('foo'), True) self.assertEqual(self.redis.get('foo'), None) def test_echo(self): self.assertEqual(self.redis.echo(b'hello'), b'hello') self.assertEqual(self.redis.echo('hello'), b'hello') @pytest.mark.slow def test_delete_expire(self): self.redis.set("foo", "bar", ex=1) self.redis.delete("foo") self.redis.set("foo", "bar") sleep(2) self.assertEqual(self.redis.get("foo"), b'bar') def test_delete_multiple(self): self.redis['one'] = 'one' self.redis['two'] = 'two' self.redis['three'] = 'three' # Since redis>=2.7.6 returns number of deleted items. self.assertEqual(self.redis.delete('one', 'two'), 2) self.assertEqual(self.redis.get('one'), None) self.assertEqual(self.redis.get('two'), None) self.assertEqual(self.redis.get('three'), b'three') self.assertEqual(self.redis.delete('one', 'two'), 0) # If any keys are deleted, True is returned. self.assertEqual(self.redis.delete('two', 'three', 'three'), 1) self.assertEqual(self.redis.get('three'), None) def test_delete_nonexistent_key(self): self.assertEqual(self.redis.delete('foo'), False) # Tests for the list type. @redis2_only def test_rpush_then_lrange_with_nested_list1(self): self.assertEqual(self.redis.rpush('foo', [12345, 6789]), 1) self.assertEqual(self.redis.rpush('foo', [54321, 9876]), 2) self.assertEqual(self.redis.lrange( 'foo', 0, -1), [b'[12345, 6789]', b'[54321, 9876]']) @redis2_only def test_rpush_then_lrange_with_nested_list2(self): self.assertEqual(self.redis.rpush('foo', [12345, 'banana']), 1) self.assertEqual(self.redis.rpush('foo', [54321, 'elephant']), 2) self.assertEqual(self.redis.lrange( 'foo', 0, -1), [b'[12345, \'banana\']', b'[54321, \'elephant\']']) @redis2_only def test_rpush_then_lrange_with_nested_list3(self): self.assertEqual(self.redis.rpush('foo', [12345, []]), 1) self.assertEqual(self.redis.rpush('foo', [54321, []]), 2) self.assertEqual(self.redis.lrange( 'foo', 0, -1), [b'[12345, []]', b'[54321, []]']) def test_lpush_then_lrange_all(self): self.assertEqual(self.redis.lpush('foo', 'bar'), 1) self.assertEqual(self.redis.lpush('foo', 'baz'), 2) self.assertEqual(self.redis.lpush('foo', 'bam', 'buzz'), 4) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'buzz', b'bam', b'baz', b'bar']) def test_lpush_then_lrange_portion(self): self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'two') self.redis.lpush('foo', 'three') self.redis.lpush('foo', 'four') self.assertEqual(self.redis.lrange('foo', 0, 2), [b'four', b'three', b'two']) self.assertEqual(self.redis.lrange('foo', 0, 3), [b'four', b'three', b'two', b'one']) def test_lrange_negative_indices(self): self.redis.rpush('foo', 'a', 'b', 'c') self.assertEqual(self.redis.lrange('foo', -1, -2), []) self.assertEqual(self.redis.lrange('foo', -2, -1), [b'b', b'c']) def test_lpush_key_does_not_exist(self): self.assertEqual(self.redis.lrange('foo', 0, -1), []) def test_lpush_with_nonstr_key(self): self.redis.lpush(1, 'one') self.redis.lpush(1, 'two') self.redis.lpush(1, 'three') self.assertEqual(self.redis.lrange(1, 0, 2), [b'three', b'two', b'one']) self.assertEqual(self.redis.lrange('1', 0, 2), [b'three', b'two', b'one']) def test_lpush_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.lpush('foo', 'element') def test_llen(self): self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'two') self.redis.lpush('foo', 'three') self.assertEqual(self.redis.llen('foo'), 3) def test_llen_no_exist(self): self.assertEqual(self.redis.llen('foo'), 0) def test_llen_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.llen('foo') def test_lrem_positive_count(self): self.redis.lpush('foo', 'same') self.redis.lpush('foo', 'same') self.redis.lpush('foo', 'different') self.redis.lrem('foo', 2, 'same') self.assertEqual(self.redis.lrange('foo', 0, -1), [b'different']) def test_lrem_negative_count(self): self.redis.lpush('foo', 'removeme') self.redis.lpush('foo', 'three') self.redis.lpush('foo', 'two') self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'removeme') self.redis.lrem('foo', -1, 'removeme') # Should remove it from the end of the list, # leaving the 'removeme' from the front of the list alone. self.assertEqual(self.redis.lrange('foo', 0, -1), [b'removeme', b'one', b'two', b'three']) def test_lrem_zero_count(self): self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lrem('foo', 0, 'one') self.assertEqual(self.redis.lrange('foo', 0, -1), []) def test_lrem_default_value(self): self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lrem('foo', 0, 'one') self.assertEqual(self.redis.lrange('foo', 0, -1), []) def test_lrem_does_not_exist(self): self.redis.lpush('foo', 'one') self.redis.lrem('foo', 0, 'one') # These should be noops. self.redis.lrem('foo', -2, 'one') self.redis.lrem('foo', 2, 'one') def test_lrem_return_value(self): self.redis.lpush('foo', 'one') count = self.redis.lrem('foo', 0, 'one') self.assertEqual(count, 1) self.assertEqual(self.redis.lrem('foo', 0, 'one'), 0) def test_lrem_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.lrem('foo', 0, 'element') def test_rpush(self): self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.redis.rpush('foo', 'three') self.redis.rpush('foo', 'four', 'five') self.assertEqual(self.redis.lrange('foo', 0, -1), [b'one', b'two', b'three', b'four', b'five']) def test_rpush_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.rpush('foo', 'element') def test_lpop(self): self.assertEqual(self.redis.rpush('foo', 'one'), 1) self.assertEqual(self.redis.rpush('foo', 'two'), 2) self.assertEqual(self.redis.rpush('foo', 'three'), 3) self.assertEqual(self.redis.lpop('foo'), b'one') self.assertEqual(self.redis.lpop('foo'), b'two') self.assertEqual(self.redis.lpop('foo'), b'three') def test_lpop_empty_list(self): self.redis.rpush('foo', 'one') self.redis.lpop('foo') self.assertEqual(self.redis.lpop('foo'), None) # Verify what happens if we try to pop from a key # we've never seen before. self.assertEqual(self.redis.lpop('noexists'), None) def test_lpop_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.lpop('foo') def test_lset(self): self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.redis.rpush('foo', 'three') self.redis.lset('foo', 0, 'four') self.redis.lset('foo', -2, 'five') self.assertEqual(self.redis.lrange('foo', 0, -1), [b'four', b'five', b'three']) def test_lset_index_out_of_range(self): self.redis.rpush('foo', 'one') with self.assertRaises(redis.ResponseError): self.redis.lset('foo', 3, 'three') def test_lset_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.lset('foo', 0, 'element') def test_rpushx(self): self.redis.rpush('foo', 'one') self.redis.rpushx('foo', 'two') self.redis.rpushx('bar', 'three') self.assertEqual(self.redis.lrange('foo', 0, -1), [b'one', b'two']) self.assertEqual(self.redis.lrange('bar', 0, -1), []) def test_rpushx_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.rpushx('foo', 'element') def test_ltrim(self): self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.redis.rpush('foo', 'three') self.redis.rpush('foo', 'four') self.assertTrue(self.redis.ltrim('foo', 1, 3)) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'two', b'three', b'four']) self.assertTrue(self.redis.ltrim('foo', 1, -1)) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'three', b'four']) def test_ltrim_with_non_existent_key(self): self.assertTrue(self.redis.ltrim('foo', 0, -1)) def test_ltrim_expiry(self): self.redis.rpush('foo', 'one', 'two', 'three') self.redis.expire('foo', 10) self.redis.ltrim('foo', 1, 2) self.assertGreater(self.redis.ttl('foo'), 0) def test_ltrim_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.ltrim('foo', 1, -1) def test_lindex(self): self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.assertEqual(self.redis.lindex('foo', 0), b'one') self.assertEqual(self.redis.lindex('foo', 4), None) self.assertEqual(self.redis.lindex('bar', 4), None) def test_lindex_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.lindex('foo', 0) def test_lpushx(self): self.redis.lpush('foo', 'two') self.redis.lpushx('foo', 'one') self.redis.lpushx('bar', 'one') self.assertEqual(self.redis.lrange('foo', 0, -1), [b'one', b'two']) self.assertEqual(self.redis.lrange('bar', 0, -1), []) def test_lpushx_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.lpushx('foo', 'element') def test_rpop(self): self.assertEqual(self.redis.rpop('foo'), None) self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.assertEqual(self.redis.rpop('foo'), b'two') self.assertEqual(self.redis.rpop('foo'), b'one') self.assertEqual(self.redis.rpop('foo'), None) def test_rpop_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.rpop('foo') def test_linsert_before(self): self.redis.rpush('foo', 'hello') self.redis.rpush('foo', 'world') self.assertEqual(self.redis.linsert('foo', 'before', 'world', 'there'), 3) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'hello', b'there', b'world']) def test_linsert_after(self): self.redis.rpush('foo', 'hello') self.redis.rpush('foo', 'world') self.assertEqual(self.redis.linsert('foo', 'after', 'hello', 'there'), 3) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'hello', b'there', b'world']) def test_linsert_no_pivot(self): self.redis.rpush('foo', 'hello') self.redis.rpush('foo', 'world') self.assertEqual(self.redis.linsert('foo', 'after', 'goodbye', 'bar'), -1) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'hello', b'world']) def test_linsert_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.linsert('foo', 'after', 'bar', 'element') def test_rpoplpush(self): self.assertEqual(self.redis.rpoplpush('foo', 'bar'), None) self.assertEqual(self.redis.lpop('bar'), None) self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.redis.rpush('bar', 'one') self.assertEqual(self.redis.rpoplpush('foo', 'bar'), b'two') self.assertEqual(self.redis.lrange('foo', 0, -1), [b'one']) self.assertEqual(self.redis.lrange('bar', 0, -1), [b'two', b'one']) # Catch instances where we store bytes and strings inconsistently # and thus bar = ['two', b'one'] self.assertEqual(self.redis.lrem('bar', -1, 'two'), 1) def test_rpoplpush_to_nonexistent_destination(self): self.redis.rpush('foo', 'one') self.assertEqual(self.redis.rpoplpush('foo', 'bar'), b'one') self.assertEqual(self.redis.rpop('bar'), b'one') def test_rpoplpush_expiry(self): self.redis.rpush('foo', 'one') self.redis.rpush('bar', 'two') self.redis.expire('bar', 10) self.redis.rpoplpush('foo', 'bar') self.assertGreater(self.redis.ttl('bar'), 0) def test_rpoplpush_one_to_self(self): self.redis.rpush('list', 'element') self.assertEqual(self.redis.brpoplpush('list', 'list'), b'element') self.assertEqual(self.redis.lrange('list', 0, -1), [b'element']) def test_rpoplpush_wrong_type(self): self.redis.set('foo', 'bar') self.redis.rpush('list', 'element') with self.assertRaises(redis.ResponseError): self.redis.rpoplpush('foo', 'list') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.lrange('list', 0, -1), [b'element']) with self.assertRaises(redis.ResponseError): self.redis.rpoplpush('list', 'foo') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.lrange('list', 0, -1), [b'element']) def test_blpop_single_list(self): self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.redis.rpush('foo', 'three') self.assertEqual(self.redis.blpop(['foo'], timeout=1), (b'foo', b'one')) def test_blpop_test_multiple_lists(self): self.redis.rpush('baz', 'zero') self.assertEqual(self.redis.blpop(['foo', 'baz'], timeout=1), (b'baz', b'zero')) self.assertFalse(self.redis.exists('baz')) self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') # bar has nothing, so the returned value should come # from foo. self.assertEqual(self.redis.blpop(['bar', 'foo'], timeout=1), (b'foo', b'one')) self.redis.rpush('bar', 'three') # bar now has something, so the returned value should come # from bar. self.assertEqual(self.redis.blpop(['bar', 'foo'], timeout=1), (b'bar', b'three')) self.assertEqual(self.redis.blpop(['bar', 'foo'], timeout=1), (b'foo', b'two')) def test_blpop_allow_single_key(self): # blpop converts single key arguments to a one element list. self.redis.rpush('foo', 'one') self.assertEqual(self.redis.blpop('foo', timeout=1), (b'foo', b'one')) @pytest.mark.slow def test_blpop_block(self): def push_thread(): sleep(0.5) self.redis.rpush('foo', 'value1') sleep(0.5) # Will wake the condition variable self.redis.set('bar', 'go back to sleep some more') self.redis.rpush('foo', 'value2') thread = threading.Thread(target=push_thread) thread.start() try: self.assertEqual(self.redis.blpop('foo'), (b'foo', b'value1')) self.assertEqual(self.redis.blpop('foo', timeout=5), (b'foo', b'value2')) finally: thread.join() def test_blpop_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.blpop('foo', timeout=1) def test_blpop_transaction(self): p = self.redis.pipeline() p.multi() p.blpop('missing', timeout=1000) result = p.execute() # Blocking commands behave like non-blocking versions in transactions self.assertEqual(result, [None]) def test_eval_blpop(self): self.redis.rpush('foo', 'bar') with self.assertRaises(redis.ResponseError) as cm: self.redis.eval('return redis.pcall("BLPOP", KEYS[1], 1)', 1, 'foo') self.assertIn('not allowed from scripts', str(cm.exception)) def test_brpop_test_multiple_lists(self): self.redis.rpush('baz', 'zero') self.assertEqual(self.redis.brpop(['foo', 'baz'], timeout=1), (b'baz', b'zero')) self.assertFalse(self.redis.exists('baz')) self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.assertEqual(self.redis.brpop(['bar', 'foo'], timeout=1), (b'foo', b'two')) def test_brpop_single_key(self): self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.assertEqual(self.redis.brpop('foo', timeout=1), (b'foo', b'two')) @pytest.mark.slow def test_brpop_block(self): def push_thread(): sleep(0.5) self.redis.rpush('foo', 'value1') sleep(0.5) # Will wake the condition variable self.redis.set('bar', 'go back to sleep some more') self.redis.rpush('foo', 'value2') thread = threading.Thread(target=push_thread) thread.start() try: self.assertEqual(self.redis.brpop('foo'), (b'foo', b'value1')) self.assertEqual(self.redis.brpop('foo', timeout=5), (b'foo', b'value2')) finally: thread.join() def test_brpop_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.brpop('foo', timeout=1) def test_brpoplpush_multi_keys(self): self.assertEqual(self.redis.lpop('bar'), None) self.redis.rpush('foo', 'one') self.redis.rpush('foo', 'two') self.assertEqual(self.redis.brpoplpush('foo', 'bar', timeout=1), b'two') self.assertEqual(self.redis.lrange('bar', 0, -1), [b'two']) # Catch instances where we store bytes and strings inconsistently # and thus bar = ['two'] self.assertEqual(self.redis.lrem('bar', -1, 'two'), 1) def test_brpoplpush_wrong_type(self): self.redis.set('foo', 'bar') self.redis.rpush('list', 'element') with self.assertRaises(redis.ResponseError): self.redis.brpoplpush('foo', 'list') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.lrange('list', 0, -1), [b'element']) with self.assertRaises(redis.ResponseError): self.redis.brpoplpush('list', 'foo') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.lrange('list', 0, -1), [b'element']) @pytest.mark.slow def test_blocking_operations_when_empty(self): self.assertEqual(self.redis.blpop(['foo'], timeout=1), None) self.assertEqual(self.redis.blpop(['bar', 'foo'], timeout=1), None) self.assertEqual(self.redis.brpop('foo', timeout=1), None) self.assertEqual(self.redis.brpoplpush('foo', 'bar', timeout=1), None) def test_empty_list(self): self.redis.rpush('foo', 'bar') self.redis.rpop('foo') self.assertFalse(self.redis.exists('foo')) # Tests for the hash type. def test_hstrlen_missing(self): self.assertEqual(self.redis.hstrlen('foo', 'doesnotexist'), 0) self.redis.hset('foo', 'key', 'value') self.assertEqual(self.redis.hstrlen('foo', 'doesnotexist'), 0) def test_hstrlen(self): self.redis.hset('foo', 'key', 'value') self.assertEqual(self.redis.hstrlen('foo', 'key'), 5) def test_hset_then_hget(self): self.assertEqual(self.redis.hset('foo', 'key', 'value'), 1) self.assertEqual(self.redis.hget('foo', 'key'), b'value') def test_hset_update(self): self.assertEqual(self.redis.hset('foo', 'key', 'value'), 1) self.assertEqual(self.redis.hset('foo', 'key', 'value'), 0) def test_hset_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hset('foo', 'key', 'value') def test_hgetall(self): self.assertEqual(self.redis.hset('foo', 'k1', 'v1'), 1) self.assertEqual(self.redis.hset('foo', 'k2', 'v2'), 1) self.assertEqual(self.redis.hset('foo', 'k3', 'v3'), 1) self.assertEqual(self.redis.hgetall('foo'), {b'k1': b'v1', b'k2': b'v2', b'k3': b'v3'}) @redis2_only def test_hgetall_with_tuples(self): self.assertEqual(self.redis.hset('foo', (1, 2), (1, 2, 3)), 1) self.assertEqual(self.redis.hgetall('foo'), {b'(1, 2)': b'(1, 2, 3)'}) def test_hgetall_empty_key(self): self.assertEqual(self.redis.hgetall('foo'), {}) def test_hgetall_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hgetall('foo') def test_hexists(self): self.redis.hset('foo', 'bar', 'v1') self.assertEqual(self.redis.hexists('foo', 'bar'), 1) self.assertEqual(self.redis.hexists('foo', 'baz'), 0) self.assertEqual(self.redis.hexists('bar', 'bar'), 0) def test_hexists_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hexists('foo', 'key') def test_hkeys(self): self.redis.hset('foo', 'k1', 'v1') self.redis.hset('foo', 'k2', 'v2') self.assertEqual(set(self.redis.hkeys('foo')), {b'k1', b'k2'}) self.assertEqual(set(self.redis.hkeys('bar')), set()) def test_hkeys_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hkeys('foo') def test_hlen(self): self.redis.hset('foo', 'k1', 'v1') self.redis.hset('foo', 'k2', 'v2') self.assertEqual(self.redis.hlen('foo'), 2) def test_hlen_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hlen('foo') def test_hvals(self): self.redis.hset('foo', 'k1', 'v1') self.redis.hset('foo', 'k2', 'v2') self.assertEqual(set(self.redis.hvals('foo')), {b'v1', b'v2'}) self.assertEqual(set(self.redis.hvals('bar')), set()) def test_hvals_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hvals('foo') def test_hmget(self): self.redis.hset('foo', 'k1', 'v1') self.redis.hset('foo', 'k2', 'v2') self.redis.hset('foo', 'k3', 'v3') # Normal case. self.assertEqual(self.redis.hmget('foo', ['k1', 'k3']), [b'v1', b'v3']) self.assertEqual(self.redis.hmget('foo', 'k1', 'k3'), [b'v1', b'v3']) # Key does not exist. self.assertEqual(self.redis.hmget('bar', ['k1', 'k3']), [None, None]) self.assertEqual(self.redis.hmget('bar', 'k1', 'k3'), [None, None]) # Some keys in the hash do not exist. self.assertEqual(self.redis.hmget('foo', ['k1', 'k500']), [b'v1', None]) self.assertEqual(self.redis.hmget('foo', 'k1', 'k500'), [b'v1', None]) def test_hmget_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hmget('foo', 'key1', 'key2') def test_hdel(self): self.redis.hset('foo', 'k1', 'v1') self.redis.hset('foo', 'k2', 'v2') self.redis.hset('foo', 'k3', 'v3') self.assertEqual(self.redis.hget('foo', 'k1'), b'v1') self.assertEqual(self.redis.hdel('foo', 'k1'), True) self.assertEqual(self.redis.hget('foo', 'k1'), None) self.assertEqual(self.redis.hdel('foo', 'k1'), False) # Since redis>=2.7.6 returns number of deleted items. self.assertEqual(self.redis.hdel('foo', 'k2', 'k3'), 2) self.assertEqual(self.redis.hget('foo', 'k2'), None) self.assertEqual(self.redis.hget('foo', 'k3'), None) self.assertEqual(self.redis.hdel('foo', 'k2', 'k3'), False) def test_hdel_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hdel('foo', 'key') def test_hincrby(self): self.redis.hset('foo', 'counter', 0) self.assertEqual(self.redis.hincrby('foo', 'counter'), 1) self.assertEqual(self.redis.hincrby('foo', 'counter'), 2) self.assertEqual(self.redis.hincrby('foo', 'counter'), 3) def test_hincrby_with_no_starting_value(self): self.assertEqual(self.redis.hincrby('foo', 'counter'), 1) self.assertEqual(self.redis.hincrby('foo', 'counter'), 2) self.assertEqual(self.redis.hincrby('foo', 'counter'), 3) def test_hincrby_with_range_param(self): self.assertEqual(self.redis.hincrby('foo', 'counter', 2), 2) self.assertEqual(self.redis.hincrby('foo', 'counter', 2), 4) self.assertEqual(self.redis.hincrby('foo', 'counter', 2), 6) def test_hincrby_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hincrby('foo', 'key', 2) def test_hincrbyfloat(self): self.redis.hset('foo', 'counter', 0.0) self.assertEqual(self.redis.hincrbyfloat('foo', 'counter'), 1.0) self.assertEqual(self.redis.hincrbyfloat('foo', 'counter'), 2.0) self.assertEqual(self.redis.hincrbyfloat('foo', 'counter'), 3.0) def test_hincrbyfloat_with_no_starting_value(self): self.assertEqual(self.redis.hincrbyfloat('foo', 'counter'), 1.0) self.assertEqual(self.redis.hincrbyfloat('foo', 'counter'), 2.0) self.assertEqual(self.redis.hincrbyfloat('foo', 'counter'), 3.0) def test_hincrbyfloat_with_range_param(self): self.assertAlmostEqual( self.redis.hincrbyfloat('foo', 'counter', 0.1), 0.1) self.assertAlmostEqual( self.redis.hincrbyfloat('foo', 'counter', 0.1), 0.2) self.assertAlmostEqual( self.redis.hincrbyfloat('foo', 'counter', 0.1), 0.3) def test_hincrbyfloat_on_non_float_value_raises_error(self): self.redis.hset('foo', 'counter', 'cat') with self.assertRaises(redis.ResponseError): self.redis.hincrbyfloat('foo', 'counter') def test_hincrbyfloat_with_non_float_amount_raises_error(self): with self.assertRaises(redis.ResponseError): self.redis.hincrbyfloat('foo', 'counter', 'cat') def test_hincrbyfloat_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hincrbyfloat('foo', 'key', 0.1) def test_hincrbyfloat_precision(self): x = 1.23456789123456789 self.assertEqual(self.redis.hincrbyfloat('foo', 'bar', x), x) self.assertEqual(float(self.redis.hget('foo', 'bar')), x) def test_hsetnx(self): self.assertEqual(self.redis.hsetnx('foo', 'newkey', 'v1'), True) self.assertEqual(self.redis.hsetnx('foo', 'newkey', 'v1'), False) self.assertEqual(self.redis.hget('foo', 'newkey'), b'v1') def test_hmsetset_empty_raises_error(self): with self.assertRaises(redis.DataError): self.redis.hmset('foo', {}) def test_hmsetset(self): self.redis.hset('foo', 'k1', 'v1') self.assertEqual(self.redis.hmset('foo', {'k2': 'v2', 'k3': 'v3'}), True) @redis2_only def test_hmset_convert_values(self): self.redis.hmset('foo', {'k1': True, 'k2': 1}) self.assertEqual( self.redis.hgetall('foo'), {b'k1': b'True', b'k2': b'1'}) @redis2_only def test_hmset_does_not_mutate_input_params(self): original = {'key': [123, 456]} self.redis.hmset('foo', original) self.assertEqual(original, {'key': [123, 456]}) def test_hmset_wrong_type(self): self.zadd('foo', {'bar': 1}) with self.assertRaises(redis.ResponseError): self.redis.hmset('foo', {'key': 'value'}) def test_empty_hash(self): self.redis.hset('foo', 'bar', 'baz') self.redis.hdel('foo', 'bar') self.assertFalse(self.redis.exists('foo')) def test_sadd(self): self.assertEqual(self.redis.sadd('foo', 'member1'), 1) self.assertEqual(self.redis.sadd('foo', 'member1'), 0) self.assertEqual(self.redis.smembers('foo'), {b'member1'}) self.assertEqual(self.redis.sadd('foo', 'member2', 'member3'), 2) self.assertEqual(self.redis.smembers('foo'), {b'member1', b'member2', b'member3'}) self.assertEqual(self.redis.sadd('foo', 'member3', 'member4'), 1) self.assertEqual(self.redis.smembers('foo'), {b'member1', b'member2', b'member3', b'member4'}) def test_sadd_as_str_type(self): self.assertEqual(self.redis.sadd('foo', *range(3)), 3) self.assertEqual(self.redis.smembers('foo'), {b'0', b'1', b'2'}) def test_sadd_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.sadd('foo', 'member2') def test_scan_single(self): self.redis.set('foo1', 'bar1') self.assertEqual(self.redis.scan(match="foo*"), (0, [b'foo1'])) def test_scan_iter_single_page(self): self.redis.set('foo1', 'bar1') self.redis.set('foo2', 'bar2') self.assertEqual(set(self.redis.scan_iter(match="foo*")), {b'foo1', b'foo2'}) self.assertEqual(set(self.redis.scan_iter()), {b'foo1', b'foo2'}) self.assertEqual(set(self.redis.scan_iter(match="")), set()) def test_scan_iter_multiple_pages(self): all_keys = key_val_dict(size=100) self.assertTrue( all(self.redis.set(k, v) for k, v in all_keys.items())) self.assertEqual( set(self.redis.scan_iter()), set(all_keys)) def test_scan_iter_multiple_pages_with_match(self): all_keys = key_val_dict(size=100) self.assertTrue( all(self.redis.set(k, v) for k, v in all_keys.items())) # Now add a few keys that don't match the key: pattern. self.redis.set('otherkey', 'foo') self.redis.set('andanother', 'bar') actual = set(self.redis.scan_iter(match='key:*')) self.assertEqual(actual, set(all_keys)) def test_scan_multiple_pages_with_count_arg(self): all_keys = key_val_dict(size=100) self.assertTrue( all(self.redis.set(k, v) for k, v in all_keys.items())) self.assertEqual( set(self.redis.scan_iter(count=1000)), set(all_keys)) def test_scan_all_in_single_call(self): all_keys = key_val_dict(size=100) self.assertTrue( all(self.redis.set(k, v) for k, v in all_keys.items())) # Specify way more than the 100 keys we've added. actual = self.redis.scan(count=1000) self.assertEqual(set(actual[1]), set(all_keys)) self.assertEqual(actual[0], 0) @pytest.mark.slow def test_scan_expired_key(self): self.redis.set('expiringkey', 'value') self.redis.pexpire('expiringkey', 1) sleep(1) self.assertEqual(self.redis.scan()[1], []) def test_scard(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('foo', 'member2') self.assertEqual(self.redis.scard('foo'), 2) def test_scard_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.scard('foo') def test_sdiff(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('bar', 'member2') self.redis.sadd('bar', 'member3') self.assertEqual(self.redis.sdiff('foo', 'bar'), {b'member1'}) # Original sets shouldn't be modified. self.assertEqual(self.redis.smembers('foo'), {b'member1', b'member2'}) self.assertEqual(self.redis.smembers('bar'), {b'member2', b'member3'}) def test_sdiff_one_key(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.assertEqual(self.redis.sdiff('foo'), {b'member1', b'member2'}) def test_sdiff_empty(self): self.assertEqual(self.redis.sdiff('foo'), set()) def test_sdiff_wrong_type(self): self.zadd('foo', {'member': 1}) self.redis.sadd('bar', 'member') with self.assertRaises(redis.ResponseError): self.redis.sdiff('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.sdiff('bar', 'foo') def test_sdiffstore(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('bar', 'member2') self.redis.sadd('bar', 'member3') self.assertEqual(self.redis.sdiffstore('baz', 'foo', 'bar'), 1) # Catch instances where we store bytes and strings inconsistently # and thus baz = {'member1', b'member1'} self.redis.sadd('baz', 'member1') self.assertEqual(self.redis.scard('baz'), 1) def test_setrange(self): self.redis.set('foo', 'test') self.assertEqual(self.redis.setrange('foo', 1, 'aste'), 5) self.assertEqual(self.redis.get('foo'), b'taste') self.redis.set('foo', 'test') self.assertEqual(self.redis.setrange('foo', 1, 'a'), 4) self.assertEqual(self.redis.get('foo'), b'tast') self.assertEqual(self.redis.setrange('bar', 2, 'test'), 6) self.assertEqual(self.redis.get('bar'), b'\x00\x00test') def test_setrange_expiry(self): self.redis.set('foo', 'test', ex=10) self.redis.setrange('foo', 1, 'aste') self.assertGreater(self.redis.ttl('foo'), 0) def test_sinter(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('bar', 'member2') self.redis.sadd('bar', 'member3') self.assertEqual(self.redis.sinter('foo', 'bar'), {b'member2'}) self.assertEqual(self.redis.sinter('foo'), {b'member1', b'member2'}) def test_sinter_bytes_keys(self): foo = os.urandom(10) bar = os.urandom(10) self.redis.sadd(foo, 'member1') self.redis.sadd(foo, 'member2') self.redis.sadd(bar, 'member2') self.redis.sadd(bar, 'member3') self.assertEqual(self.redis.sinter(foo, bar), {b'member2'}) self.assertEqual(self.redis.sinter(foo), {b'member1', b'member2'}) def test_sinter_wrong_type(self): self.zadd('foo', {'member': 1}) self.redis.sadd('bar', 'member') with self.assertRaises(redis.ResponseError): self.redis.sinter('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.sinter('bar', 'foo') def test_sinterstore(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('bar', 'member2') self.redis.sadd('bar', 'member3') self.assertEqual(self.redis.sinterstore('baz', 'foo', 'bar'), 1) # Catch instances where we store bytes and strings inconsistently # and thus baz = {'member2', b'member2'} self.redis.sadd('baz', 'member2') self.assertEqual(self.redis.scard('baz'), 1) def test_sismember(self): self.assertEqual(self.redis.sismember('foo', 'member1'), False) self.redis.sadd('foo', 'member1') self.assertEqual(self.redis.sismember('foo', 'member1'), True) def test_sismember_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.sismember('foo', 'member') def test_smembers(self): self.assertEqual(self.redis.smembers('foo'), set()) def test_smembers_copy(self): self.redis.sadd('foo', 'member1') set = self.redis.smembers('foo') self.redis.sadd('foo', 'member2') self.assertNotEqual(set, self.redis.smembers('foo')) def test_smembers_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.smembers('foo') def test_smembers_runtime_error(self): self.redis.sadd('foo', 'member1', 'member2') for member in self.redis.smembers('foo'): self.redis.srem('foo', member) def test_smove(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.assertEqual(self.redis.smove('foo', 'bar', 'member1'), True) self.assertEqual(self.redis.smembers('bar'), {b'member1'}) def test_smove_non_existent_key(self): self.assertEqual(self.redis.smove('foo', 'bar', 'member1'), False) def test_smove_wrong_type(self): self.zadd('foo', {'member': 1}) self.redis.sadd('bar', 'member') with self.assertRaises(redis.ResponseError): self.redis.smove('bar', 'foo', 'member') # Must raise the error before removing member from bar self.assertEqual(self.redis.smembers('bar'), {b'member'}) with self.assertRaises(redis.ResponseError): self.redis.smove('foo', 'bar', 'member') def test_spop(self): # This is tricky because it pops a random element. self.redis.sadd('foo', 'member1') self.assertEqual(self.redis.spop('foo'), b'member1') self.assertEqual(self.redis.spop('foo'), None) def test_spop_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.spop('foo') def test_srandmember(self): self.redis.sadd('foo', 'member1') self.assertEqual(self.redis.srandmember('foo'), b'member1') # Shouldn't be removed from the set. self.assertEqual(self.redis.srandmember('foo'), b'member1') def test_srandmember_number(self): """srandmember works with the number argument.""" self.assertEqual(self.redis.srandmember('foo', 2), []) self.redis.sadd('foo', b'member1') self.assertEqual(self.redis.srandmember('foo', 2), [b'member1']) self.redis.sadd('foo', b'member2') self.assertEqual(set(self.redis.srandmember('foo', 2)), {b'member1', b'member2'}) self.redis.sadd('foo', b'member3') res = self.redis.srandmember('foo', 2) self.assertEqual(len(res), 2) if self.decode_responses: superset = {'member1', 'member2', 'member3'} else: superset = {b'member1', b'member2', b'member3'} for e in res: self.assertIn(e, superset) def test_srandmember_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.srandmember('foo') def test_srem(self): self.redis.sadd('foo', 'member1', 'member2', 'member3', 'member4') self.assertEqual(self.redis.smembers('foo'), {b'member1', b'member2', b'member3', b'member4'}) self.assertEqual(self.redis.srem('foo', 'member1'), True) self.assertEqual(self.redis.smembers('foo'), {b'member2', b'member3', b'member4'}) self.assertEqual(self.redis.srem('foo', 'member1'), False) # Since redis>=2.7.6 returns number of deleted items. self.assertEqual(self.redis.srem('foo', 'member2', 'member3'), 2) self.assertEqual(self.redis.smembers('foo'), {b'member4'}) self.assertEqual(self.redis.srem('foo', 'member3', 'member4'), True) self.assertEqual(self.redis.smembers('foo'), set()) self.assertEqual(self.redis.srem('foo', 'member3', 'member4'), False) def test_srem_wrong_type(self): self.zadd('foo', {'member': 1}) with self.assertRaises(redis.ResponseError): self.redis.srem('foo', 'member') def test_sunion(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('bar', 'member2') self.redis.sadd('bar', 'member3') self.assertEqual(self.redis.sunion('foo', 'bar'), {b'member1', b'member2', b'member3'}) def test_sunion_wrong_type(self): self.zadd('foo', {'member': 1}) self.redis.sadd('bar', 'member') with self.assertRaises(redis.ResponseError): self.redis.sunion('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.sunion('bar', 'foo') def test_sunionstore(self): self.redis.sadd('foo', 'member1') self.redis.sadd('foo', 'member2') self.redis.sadd('bar', 'member2') self.redis.sadd('bar', 'member3') self.assertEqual(self.redis.sunionstore('baz', 'foo', 'bar'), 3) self.assertEqual(self.redis.smembers('baz'), {b'member1', b'member2', b'member3'}) # Catch instances where we store bytes and strings inconsistently # and thus baz = {b'member1', b'member2', b'member3', 'member3'} self.redis.sadd('baz', 'member3') self.assertEqual(self.redis.scard('baz'), 3) def test_empty_set(self): self.redis.sadd('foo', 'bar') self.redis.srem('foo', 'bar') self.assertFalse(self.redis.exists('foo')) def test_zadd(self): self.zadd('foo', {'four': 4}) self.zadd('foo', {'three': 3}) self.assertEqual(self.zadd('foo', {'two': 2, 'one': 1, 'zero': 0}), 3) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'zero', b'one', b'two', b'three', b'four']) self.assertEqual(self.zadd('foo', {'zero': 7, 'one': 1, 'five': 5}), 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'one', b'two', b'three', b'four', b'five', b'zero']) @redis2_only def test_zadd_uses_str(self): self.redis.zadd('foo', 12345, (1, 2, 3)) self.assertEqual(self.redis.zrange('foo', 0, 0), [b'(1, 2, 3)']) @redis2_only def test_zadd_errors(self): # The args are backwards, it should be 2, "two", so we # expect an exception to be raised. with self.assertRaises(redis.ResponseError): self.redis.zadd('foo', 'two', 2) with self.assertRaises(redis.ResponseError): self.redis.zadd('foo', two='two') # It's expected an equal number of values and scores with self.assertRaises(redis.RedisError): self.redis.zadd('foo', 'two') def test_zadd_empty(self): # Have to add at least one key/value pair with self.assertRaises(redis.RedisError): self.zadd('foo', {}) def test_zadd_minus_zero(self): # Changing -0 to +0 is ignored self.zadd('foo', {'a': -0.0}) self.zadd('foo', {'a': 0.0}) self.assertEqual(self.raw_command('zscore', 'foo', 'a'), b'-0') def test_zadd_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.zadd('foo', {'two': 2}) def test_zadd_multiple(self): self.zadd('foo', {'one': 1, 'two': 2}) self.assertEqual(self.redis.zrange('foo', 0, 0), [b'one']) self.assertEqual(self.redis.zrange('foo', 1, 1), [b'two']) @redis3_only def test_zadd_with_nx(self): self.zadd('foo', {'four': 4.0, 'three': 3.0}) updates = [ UpdateCommand( input={'four': 2.0, 'three': 1.0}, expected_return_value=0, expected_state=[(b'four', 4.0), (b'three', 3.0)]), UpdateCommand( input={'four': 2.0, 'three': 1.0, 'zero': 0.0}, expected_return_value=1, expected_state=[(b'four', 4.0), (b'three', 3.0), (b'zero', 0.0)]), UpdateCommand( input={'two': 2.0, 'one': 1.0}, expected_return_value=2, expected_state=[(b'four', 4.0), (b'three', 3.0), (b'two', 2.0), (b'one', 1.0), (b'zero', 0.0)]), ] for update in updates: self.assertEqual(self.zadd('foo', update.input, nx=True), update.expected_return_value) self.assertItemsEqual(self.redis.zrange('foo', 0, -1, withscores=True), update.expected_state) @redis3_only def test_zadd_with_ch(self): self.zadd('foo', {'four': 4.0, 'three': 3.0}) updates = [ UpdateCommand( input={'four': 4.0, 'three': 1.0}, expected_return_value=1, expected_state=[(b'four', 4.0), (b'three', 1.0)]), UpdateCommand( input={'four': 4.0, 'three': 3.0, 'zero': 0.0}, expected_return_value=2, expected_state=[(b'four', 4.0), (b'three', 3.0), (b'zero', 0.0)]), UpdateCommand( input={'two': 2.0, 'one': 1.0}, expected_return_value=2, expected_state=[(b'four', 4.0), (b'three', 3.0), (b'two', 2.0), (b'one', 1.0), (b'zero', 0.0)]), ] for update in updates: self.assertEqual(self.zadd('foo', update.input, ch=True), update.expected_return_value) self.assertItemsEqual(self.redis.zrange('foo', 0, -1, withscores=True), update.expected_state) @redis3_only def test_zadd_with_xx(self): self.zadd('foo', {'four': 4.0, 'three': 3.0}) updates = [ UpdateCommand( input={'four': 2.0, 'three': 1.0}, expected_return_value=0, expected_state=[(b'four', 2.0), (b'three', 1.0)]), UpdateCommand( input={'four': 4.0, 'three': 3.0, 'zero': 0.0}, expected_return_value=0, expected_state=[(b'four', 4.0), (b'three', 3.0)]), UpdateCommand( input={'two': 2.0, 'one': 1.0}, expected_return_value=0, expected_state=[(b'four', 4.0), (b'three', 3.0)]), ] for update in updates: self.assertEqual(self.zadd('foo', update.input, xx=True), update.expected_return_value) self.assertItemsEqual(self.redis.zrange('foo', 0, -1, withscores=True), update.expected_state) @redis3_only def test_zadd_with_nx_and_xx(self): self.zadd('foo', {'four': 4.0, 'three': 3.0}) with self.assertRaises(redis.DataError): self.zadd('foo', {'four': -4.0, 'three': -3.0}, nx=True, xx=True) with self.assertRaises(redis.DataError): self.zadd('foo', {'four': -4.0, 'three': -3.0}, nx=True, xx=True, ch=True) @redis3_only def test_zadd_with_nx_and_ch(self): self.zadd('foo', {'four': 4.0, 'three': 3.0}) updates = [ UpdateCommand( input={'four': 2.0, 'three': 1.0}, expected_return_value=0, expected_state=[(b'four', 4.0), (b'three', 3.0)]), UpdateCommand( input={'four': 2.0, 'three': 1.0, 'zero': 0.0}, expected_return_value=1, expected_state=[(b'four', 4.0), (b'three', 3.0), (b'zero', 0.0)]), UpdateCommand( input={'two': 2.0, 'one': 1.0}, expected_return_value=2, expected_state=[(b'four', 4.0), (b'three', 3.0), (b'two', 2.0), (b'one', 1.0), (b'zero', 0.0)]), ] for update in updates: self.assertEqual(self.zadd('foo', update.input, nx=True, ch=True), update.expected_return_value) self.assertItemsEqual(self.redis.zrange('foo', 0, -1, withscores=True), update.expected_state) @redis3_only def test_zadd_with_xx_and_ch(self): self.zadd('foo', {'four': 4.0, 'three': 3.0}) updates = [ UpdateCommand( input={'four': 2.0, 'three': 1.0}, expected_return_value=2, expected_state=[(b'four', 2.0), (b'three', 1.0)]), UpdateCommand( input={'four': 4.0, 'three': 3.0, 'zero': 0.0}, expected_return_value=2, expected_state=[(b'four', 4.0), (b'three', 3.0)]), UpdateCommand( input={'two': 2.0, 'one': 1.0}, expected_return_value=0, expected_state=[(b'four', 4.0), (b'three', 3.0)]), ] for update in updates: self.assertEqual(self.zadd('foo', update.input, xx=True, ch=True), update.expected_return_value) self.assertItemsEqual(self.redis.zrange('foo', 0, -1, withscores=True), update.expected_state) def test_zrange_same_score(self): self.zadd('foo', {'two_a': 2}) self.zadd('foo', {'two_b': 2}) self.zadd('foo', {'two_c': 2}) self.zadd('foo', {'two_d': 2}) self.zadd('foo', {'two_e': 2}) self.assertEqual(self.redis.zrange('foo', 2, 3), [b'two_c', b'two_d']) def test_zcard(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.assertEqual(self.redis.zcard('foo'), 2) def test_zcard_non_existent_key(self): self.assertEqual(self.redis.zcard('foo'), 0) def test_zcard_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zcard('foo') def test_zcount(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'three': 2}) self.zadd('foo', {'five': 5}) self.assertEqual(self.redis.zcount('foo', 2, 4), 1) self.assertEqual(self.redis.zcount('foo', 1, 4), 2) self.assertEqual(self.redis.zcount('foo', 0, 5), 3) self.assertEqual(self.redis.zcount('foo', 4, '+inf'), 1) self.assertEqual(self.redis.zcount('foo', '-inf', 4), 2) self.assertEqual(self.redis.zcount('foo', '-inf', '+inf'), 3) def test_zcount_exclusive(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'three': 2}) self.zadd('foo', {'five': 5}) self.assertEqual(self.redis.zcount('foo', '-inf', '(2'), 1) self.assertEqual(self.redis.zcount('foo', '-inf', 2), 2) self.assertEqual(self.redis.zcount('foo', '(5', '+inf'), 0) self.assertEqual(self.redis.zcount('foo', '(1', 5), 2) self.assertEqual(self.redis.zcount('foo', '(2', '(5'), 0) self.assertEqual(self.redis.zcount('foo', '(1', '(5'), 1) self.assertEqual(self.redis.zcount('foo', 2, '(5'), 1) def test_zcount_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zcount('foo', '-inf', '+inf') def test_zincrby(self): self.zadd('foo', {'one': 1}) self.assertEqual(self.zincrby('foo', 10, 'one'), 11) self.assertEqual(self.redis.zrange('foo', 0, -1, withscores=True), [(b'one', 11)]) def test_zincrby_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.zincrby('foo', 10, 'one') def test_zrange_descending(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrange('foo', 0, -1, desc=True), [b'three', b'two', b'one']) def test_zrange_descending_with_scores(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrange('foo', 0, -1, desc=True, withscores=True), [(b'three', 3), (b'two', 2), (b'one', 1)]) def test_zrange_with_positive_indices(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrange('foo', 0, 1), [b'one', b'two']) def test_zrange_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrange('foo', 0, -1) def test_zrange_score_cast(self): self.zadd('foo', {'one': 1.2}) self.zadd('foo', {'two': 2.2}) expected_without_cast_round = [(b'one', 1.2), (b'two', 2.2)] expected_with_cast_round = [(b'one', 1.0), (b'two', 2.0)] self.assertEqual(self.redis.zrange('foo', 0, 2, withscores=True), expected_without_cast_round) self.assertEqual(self.redis.zrange('foo', 0, 2, withscores=True, score_cast_func=self._round_str), expected_with_cast_round) def test_zrank(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrank('foo', 'one'), 0) self.assertEqual(self.redis.zrank('foo', 'two'), 1) self.assertEqual(self.redis.zrank('foo', 'three'), 2) def test_zrank_non_existent_member(self): self.assertEqual(self.redis.zrank('foo', 'one'), None) def test_zrank_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrank('foo', 'one') def test_zrem(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.zadd('foo', {'four': 4}) self.assertEqual(self.redis.zrem('foo', 'one'), True) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'two', b'three', b'four']) # Since redis>=2.7.6 returns number of deleted items. self.assertEqual(self.redis.zrem('foo', 'two', 'three'), 2) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'four']) self.assertEqual(self.redis.zrem('foo', 'three', 'four'), True) self.assertEqual(self.redis.zrange('foo', 0, -1), []) self.assertEqual(self.redis.zrem('foo', 'three', 'four'), False) def test_zrem_non_existent_member(self): self.assertFalse(self.redis.zrem('foo', 'one')) def test_zrem_numeric_member(self): self.zadd('foo', {'128': 13.0, '129': 12.0}) self.assertEqual(self.redis.zrem('foo', 128), True) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'129']) def test_zrem_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrem('foo', 'bar') def test_zscore(self): self.zadd('foo', {'one': 54}) self.assertEqual(self.redis.zscore('foo', 'one'), 54) def test_zscore_non_existent_member(self): self.assertIsNone(self.redis.zscore('foo', 'one')) def test_zscore_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zscore('foo', 'one') def test_zrevrank(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrevrank('foo', 'one'), 2) self.assertEqual(self.redis.zrevrank('foo', 'two'), 1) self.assertEqual(self.redis.zrevrank('foo', 'three'), 0) def test_zrevrank_non_existent_member(self): self.assertEqual(self.redis.zrevrank('foo', 'one'), None) def test_zrevrank_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrevrank('foo', 'one') def test_zrevrange(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrevrange('foo', 0, 1), [b'three', b'two']) self.assertEqual(self.redis.zrevrange('foo', 0, -1), [b'three', b'two', b'one']) def test_zrevrange_sorted_keys(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'two_b': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrevrange('foo', 0, 2), [b'three', b'two_b', b'two']) self.assertEqual(self.redis.zrevrange('foo', 0, -1), [b'three', b'two_b', b'two', b'one']) def test_zrevrange_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrevrange('foo', 0, 2) def test_zrevrange_score_cast(self): self.zadd('foo', {'one': 1.2}) self.zadd('foo', {'two': 2.2}) expected_without_cast_round = [(b'two', 2.2), (b'one', 1.2)] expected_with_cast_round = [(b'two', 2.0), (b'one', 1.0)] self.assertEqual(self.redis.zrevrange('foo', 0, 2, withscores=True), expected_without_cast_round) self.assertEqual(self.redis.zrevrange('foo', 0, 2, withscores=True, score_cast_func=self._round_str), expected_with_cast_round) def test_zrangebyscore(self): self.zadd('foo', {'zero': 0}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'two_a_also': 2}) self.zadd('foo', {'two_b_also': 2}) self.zadd('foo', {'four': 4}) self.assertEqual(self.redis.zrangebyscore('foo', 1, 3), [b'two', b'two_a_also', b'two_b_also']) self.assertEqual(self.redis.zrangebyscore('foo', 2, 3), [b'two', b'two_a_also', b'two_b_also']) self.assertEqual(self.redis.zrangebyscore('foo', 0, 4), [b'zero', b'two', b'two_a_also', b'two_b_also', b'four']) self.assertEqual(self.redis.zrangebyscore('foo', '-inf', 1), [b'zero']) self.assertEqual(self.redis.zrangebyscore('foo', 2, '+inf'), [b'two', b'two_a_also', b'two_b_also', b'four']) self.assertEqual(self.redis.zrangebyscore('foo', '-inf', '+inf'), [b'zero', b'two', b'two_a_also', b'two_b_also', b'four']) def test_zrangebysore_exclusive(self): self.zadd('foo', {'zero': 0}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'four': 4}) self.zadd('foo', {'five': 5}) self.assertEqual(self.redis.zrangebyscore('foo', '(0', 6), [b'two', b'four', b'five']) self.assertEqual(self.redis.zrangebyscore('foo', '(2', '(5'), [b'four']) self.assertEqual(self.redis.zrangebyscore('foo', 0, '(4'), [b'zero', b'two']) def test_zrangebyscore_raises_error(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) with self.assertRaises(redis.ResponseError): self.redis.zrangebyscore('foo', 'one', 2) with self.assertRaises(redis.ResponseError): self.redis.zrangebyscore('foo', 2, 'three') with self.assertRaises(redis.ResponseError): self.redis.zrangebyscore('foo', 2, '3)') with self.assertRaises(redis.RedisError): self.redis.zrangebyscore('foo', 2, '3)', 0, None) def test_zrangebyscore_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrangebyscore('foo', '(1', '(2') def test_zrangebyscore_slice(self): self.zadd('foo', {'two_a': 2}) self.zadd('foo', {'two_b': 2}) self.zadd('foo', {'two_c': 2}) self.zadd('foo', {'two_d': 2}) self.assertEqual(self.redis.zrangebyscore('foo', 0, 4, 0, 2), [b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebyscore('foo', 0, 4, 1, 3), [b'two_b', b'two_c', b'two_d']) def test_zrangebyscore_withscores(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrangebyscore('foo', 1, 3, 0, 2, True), [(b'one', 1), (b'two', 2)]) def test_zrangebyscore_cast_scores(self): self.zadd('foo', {'two': 2}) self.zadd('foo', {'two_a_also': 2.2}) expected_without_cast_round = [(b'two', 2.0), (b'two_a_also', 2.2)] expected_with_cast_round = [(b'two', 2.0), (b'two_a_also', 2.0)] self.assertItemsEqual( self.redis.zrangebyscore('foo', 2, 3, withscores=True), expected_without_cast_round ) self.assertItemsEqual( self.redis.zrangebyscore('foo', 2, 3, withscores=True, score_cast_func=self._round_str), expected_with_cast_round ) def test_zrevrangebyscore(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrevrangebyscore('foo', 3, 1), [b'three', b'two', b'one']) self.assertEqual(self.redis.zrevrangebyscore('foo', 3, 2), [b'three', b'two']) self.assertEqual(self.redis.zrevrangebyscore('foo', 3, 1, 0, 1), [b'three']) self.assertEqual(self.redis.zrevrangebyscore('foo', 3, 1, 1, 2), [b'two', b'one']) def test_zrevrangebyscore_exclusive(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zrevrangebyscore('foo', '(3', 1), [b'two', b'one']) self.assertEqual(self.redis.zrevrangebyscore('foo', 3, '(2'), [b'three']) self.assertEqual(self.redis.zrevrangebyscore('foo', '(3', '(1'), [b'two']) self.assertEqual(self.redis.zrevrangebyscore('foo', '(2', 1, 0, 1), [b'one']) self.assertEqual(self.redis.zrevrangebyscore('foo', '(2', '(1', 0, 1), []) self.assertEqual(self.redis.zrevrangebyscore('foo', '(3', '(0', 1, 2), [b'one']) def test_zrevrangebyscore_raises_error(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) with self.assertRaises(redis.ResponseError): self.redis.zrevrangebyscore('foo', 'three', 1) with self.assertRaises(redis.ResponseError): self.redis.zrevrangebyscore('foo', 3, 'one') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebyscore('foo', 3, '1)') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebyscore('foo', '((3', '1)') def test_zrevrangebyscore_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebyscore('foo', '(3', '(1') def test_zrevrangebyscore_cast_scores(self): self.zadd('foo', {'two': 2}) self.zadd('foo', {'two_a_also': 2.2}) expected_without_cast_round = [(b'two_a_also', 2.2), (b'two', 2.0)] expected_with_cast_round = [(b'two_a_also', 2.0), (b'two', 2.0)] self.assertEqual( self.redis.zrevrangebyscore('foo', 3, 2, withscores=True), expected_without_cast_round ) self.assertEqual( self.redis.zrevrangebyscore('foo', 3, 2, withscores=True, score_cast_func=self._round_str), expected_with_cast_round ) def test_zrangebylex(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) self.assertEqual(self.redis.zrangebylex('foo', b'(t', b'+'), [b'three_a', b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebylex('foo', b'(t', b'[two_b'), [b'three_a', b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebylex('foo', b'(t', b'(two_b'), [b'three_a', b'two_a']) self.assertEqual(self.redis.zrangebylex('foo', b'[three_a', b'[two_b'), [b'three_a', b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebylex('foo', b'(three_a', b'[two_b'), [b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebylex('foo', b'-', b'(two_b'), [b'one_a', b'three_a', b'two_a']) self.assertEqual(self.redis.zrangebylex('foo', b'[two_b', b'(two_b'), []) # reversed max + and min - boundaries # these will be always empty, but allowed by redis self.assertEqual(self.redis.zrangebylex('foo', b'+', b'-'), []) self.assertEqual(self.redis.zrangebylex('foo', b'+', b'[three_a'), []) self.assertEqual(self.redis.zrangebylex('foo', b'[o', b'-'), []) def test_zrangebylex_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrangebylex('foo', b'-', b'+') def test_zlexcount(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) self.assertEqual(self.redis.zlexcount('foo', b'(t', b'+'), 3) self.assertEqual(self.redis.zlexcount('foo', b'(t', b'[two_b'), 3) self.assertEqual(self.redis.zlexcount('foo', b'(t', b'(two_b'), 2) self.assertEqual(self.redis.zlexcount('foo', b'[three_a', b'[two_b'), 3) self.assertEqual(self.redis.zlexcount('foo', b'(three_a', b'[two_b'), 2) self.assertEqual(self.redis.zlexcount('foo', b'-', b'(two_b'), 3) self.assertEqual(self.redis.zlexcount('foo', b'[two_b', b'(two_b'), 0) # reversed max + and min - boundaries # these will be always empty, but allowed by redis self.assertEqual(self.redis.zlexcount('foo', b'+', b'-'), 0) self.assertEqual(self.redis.zlexcount('foo', b'+', b'[three_a'), 0) self.assertEqual(self.redis.zlexcount('foo', b'[o', b'-'), 0) def test_zlexcount_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zlexcount('foo', b'-', b'+') def test_zrangebylex_with_limit(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) self.assertEqual(self.redis.zrangebylex('foo', b'-', b'+', 1, 2), [b'three_a', b'two_a']) # negative offset no results self.assertEqual(self.redis.zrangebylex('foo', b'-', b'+', -1, 3), []) # negative limit ignored self.assertEqual(self.redis.zrangebylex('foo', b'-', b'+', 0, -2), [b'one_a', b'three_a', b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebylex('foo', b'-', b'+', 1, -2), [b'three_a', b'two_a', b'two_b']) self.assertEqual(self.redis.zrangebylex('foo', b'+', b'-', 1, 1), []) def test_zrangebylex_raises_error(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) with self.assertRaises(redis.ResponseError): self.redis.zrangebylex('foo', b'', b'[two_b') with self.assertRaises(redis.ResponseError): self.redis.zrangebylex('foo', b'-', b'two_b') with self.assertRaises(redis.ResponseError): self.redis.zrangebylex('foo', b'(t', b'two_b') with self.assertRaises(redis.ResponseError): self.redis.zrangebylex('foo', b't', b'+') with self.assertRaises(redis.ResponseError): self.redis.zrangebylex('foo', b'[two_a', b'') with self.assertRaises(redis.RedisError): self.redis.zrangebylex('foo', b'(two_a', b'[two_b', 1) def test_zrevrangebylex(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) self.assertEqual(self.redis.zrevrangebylex('foo', b'+', b'(t'), [b'two_b', b'two_a', b'three_a']) self.assertEqual(self.redis.zrevrangebylex('foo', b'[two_b', b'(t'), [b'two_b', b'two_a', b'three_a']) self.assertEqual(self.redis.zrevrangebylex('foo', b'(two_b', b'(t'), [b'two_a', b'three_a']) self.assertEqual(self.redis.zrevrangebylex('foo', b'[two_b', b'[three_a'), [b'two_b', b'two_a', b'three_a']) self.assertEqual(self.redis.zrevrangebylex('foo', b'[two_b', b'(three_a'), [b'two_b', b'two_a']) self.assertEqual(self.redis.zrevrangebylex('foo', b'(two_b', b'-'), [b'two_a', b'three_a', b'one_a']) self.assertEqual(self.redis.zrangebylex('foo', b'(two_b', b'[two_b'), []) # reversed max + and min - boundaries # these will be always empty, but allowed by redis self.assertEqual(self.redis.zrevrangebylex('foo', b'-', b'+'), []) self.assertEqual(self.redis.zrevrangebylex('foo', b'[three_a', b'+'), []) self.assertEqual(self.redis.zrevrangebylex('foo', b'-', b'[o'), []) def test_zrevrangebylex_with_limit(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) self.assertEqual(self.redis.zrevrangebylex('foo', b'+', b'-', 1, 2), [b'two_a', b'three_a']) def test_zrevrangebylex_raises_error(self): self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'three_a': 0}) with self.assertRaises(redis.ResponseError): self.redis.zrevrangebylex('foo', b'[two_b', b'') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebylex('foo', b'two_b', b'-') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebylex('foo', b'two_b', b'(t') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebylex('foo', b'+', b't') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebylex('foo', b'', b'[two_a') with self.assertRaises(redis.RedisError): self.redis.zrevrangebylex('foo', b'[two_a', b'(two_b', 1) def test_zrevrangebylex_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zrevrangebylex('foo', b'+', b'-') def test_zremrangebyrank(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zremrangebyrank('foo', 0, 1), 2) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'three']) def test_zremrangebyrank_negative_indices(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'three': 3}) self.assertEqual(self.redis.zremrangebyrank('foo', -2, -1), 2) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'one']) def test_zremrangebyrank_out_of_bounds(self): self.zadd('foo', {'one': 1}) self.assertEqual(self.redis.zremrangebyrank('foo', 1, 3), 0) def test_zremrangebyrank_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zremrangebyrank('foo', 1, 3) def test_zremrangebyscore(self): self.zadd('foo', {'zero': 0}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'four': 4}) # Outside of range. self.assertEqual(self.redis.zremrangebyscore('foo', 5, 10), 0) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'zero', b'two', b'four']) # Middle of range. self.assertEqual(self.redis.zremrangebyscore('foo', 1, 3), 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'zero', b'four']) self.assertEqual(self.redis.zremrangebyscore('foo', 1, 3), 0) # Entire range. self.assertEqual(self.redis.zremrangebyscore('foo', 0, 4), 2) self.assertEqual(self.redis.zrange('foo', 0, -1), []) def test_zremrangebyscore_exclusive(self): self.zadd('foo', {'zero': 0}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'four': 4}) self.assertEqual(self.redis.zremrangebyscore('foo', '(0', 1), 0) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'zero', b'two', b'four']) self.assertEqual(self.redis.zremrangebyscore('foo', '-inf', '(0'), 0) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'zero', b'two', b'four']) self.assertEqual(self.redis.zremrangebyscore('foo', '(2', 5), 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'zero', b'two']) self.assertEqual(self.redis.zremrangebyscore('foo', 0, '(2'), 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'two']) self.assertEqual(self.redis.zremrangebyscore('foo', '(1', '(3'), 1) self.assertEqual(self.redis.zrange('foo', 0, -1), []) def test_zremrangebyscore_raises_error(self): self.zadd('foo', {'zero': 0}) self.zadd('foo', {'two': 2}) self.zadd('foo', {'four': 4}) with self.assertRaises(redis.ResponseError): self.redis.zremrangebyscore('foo', 'three', 1) with self.assertRaises(redis.ResponseError): self.redis.zremrangebyscore('foo', 3, 'one') with self.assertRaises(redis.ResponseError): self.redis.zremrangebyscore('foo', 3, '1)') with self.assertRaises(redis.ResponseError): self.redis.zremrangebyscore('foo', '((3', '1)') def test_zremrangebyscore_badkey(self): self.assertEqual(self.redis.zremrangebyscore('foo', 0, 2), 0) def test_zremrangebyscore_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zremrangebyscore('foo', 0, 2) def test_zremrangebylex(self): self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'three_a': 0}) self.assertEqual(self.redis.zremrangebylex('foo', b'(three_a', b'[two_b'), 2) self.assertEqual(self.redis.zremrangebylex('foo', b'(three_a', b'[two_b'), 0) self.assertEqual(self.redis.zremrangebylex('foo', b'-', b'(o'), 0) self.assertEqual(self.redis.zremrangebylex('foo', b'-', b'[one_a'), 1) self.assertEqual(self.redis.zremrangebylex('foo', b'[tw', b'+'), 0) self.assertEqual(self.redis.zremrangebylex('foo', b'[t', b'+'), 1) self.assertEqual(self.redis.zremrangebylex('foo', b'[t', b'+'), 0) def test_zremrangebylex_error(self): self.zadd('foo', {'two_a': 0}) self.zadd('foo', {'two_b': 0}) self.zadd('foo', {'one_a': 0}) self.zadd('foo', {'three_a': 0}) with self.assertRaises(redis.ResponseError): self.redis.zremrangebylex('foo', b'(t', b'two_b') with self.assertRaises(redis.ResponseError): self.redis.zremrangebylex('foo', b't', b'+') with self.assertRaises(redis.ResponseError): self.redis.zremrangebylex('foo', b'[two_a', b'') def test_zremrangebylex_badkey(self): self.assertEqual(self.redis.zremrangebylex('foo', b'(three_a', b'[two_b'), 0) def test_zremrangebylex_wrong_type(self): self.redis.sadd('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zremrangebylex('foo', b'bar', b'baz') def test_zunionstore(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zunionstore('baz', ['foo', 'bar']) self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 2), (b'three', 3), (b'two', 4)]) def test_zunionstore_sum(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zunionstore('baz', ['foo', 'bar'], aggregate='SUM') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 2), (b'three', 3), (b'two', 4)]) def test_zunionstore_max(self): self.zadd('foo', {'one': 0}) self.zadd('foo', {'two': 0}) self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zunionstore('baz', ['foo', 'bar'], aggregate='MAX') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 1), (b'two', 2), (b'three', 3)]) def test_zunionstore_min(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('bar', {'one': 0}) self.zadd('bar', {'two': 0}) self.zadd('bar', {'three': 3}) self.redis.zunionstore('baz', ['foo', 'bar'], aggregate='MIN') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 0), (b'two', 0), (b'three', 3)]) def test_zunionstore_weights(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'four': 4}) self.redis.zunionstore('baz', {'foo': 1, 'bar': 2}, aggregate='SUM') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 3), (b'two', 6), (b'four', 8)]) def test_zunionstore_nan_to_zero(self): self.zadd('foo', {'x': math.inf}) self.zadd('foo2', {'x': math.inf}) self.redis.zunionstore('bar', OrderedDict([('foo', 1.0), ('foo2', 0.0)])) # This is different to test_zinterstore_nan_to_zero because of a quirk # in redis. See https://github.com/antirez/redis/issues/3954. self.assertEqual(self.redis.zscore('bar', 'x'), math.inf) def test_zunionstore_nan_to_zero2(self): self.zadd('foo', {'zero': 0}) self.zadd('foo2', {'one': 1}) self.zadd('foo3', {'one': 1}) self.redis.zunionstore('bar', {'foo': math.inf}, aggregate='SUM') self.assertEqual(self.redis.zrange('bar', 0, -1, withscores=True), [(b'zero', 0)]) self.redis.zunionstore('bar', OrderedDict([('foo2', math.inf), ('foo3', -math.inf)])) self.assertEqual(self.redis.zrange('bar', 0, -1, withscores=True), [(b'one', 0)]) def test_zunionstore_nan_to_zero_ordering(self): self.zadd('foo', {'e1': math.inf}) self.zadd('bar', {'e1': -math.inf, 'e2': 0.0}) self.redis.zunionstore('baz', ['foo', 'bar', 'foo']) self.assertEqual(self.redis.zscore('baz', 'e1'), 0.0) def test_zunionstore_mixed_set_types(self): # No score, redis will use 1.0. self.redis.sadd('foo', 'one') self.redis.sadd('foo', 'two') self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zunionstore('baz', ['foo', 'bar'], aggregate='SUM') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 2), (b'three', 3), (b'two', 3)]) def test_zunionstore_badkey(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.redis.zunionstore('baz', ['foo', 'bar'], aggregate='SUM') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 1), (b'two', 2)]) self.redis.zunionstore('baz', {'foo': 1, 'bar': 2}, aggregate='SUM') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 1), (b'two', 2)]) def test_zunionstore_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zunionstore('baz', ['foo', 'bar']) def test_zinterstore(self): self.zadd('foo', {'one': 1}) self.zadd('foo', {'two': 2}) self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zinterstore('baz', ['foo', 'bar']) self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 2), (b'two', 4)]) def test_zinterstore_mixed_set_types(self): self.redis.sadd('foo', 'one') self.redis.sadd('foo', 'two') self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zinterstore('baz', ['foo', 'bar'], aggregate='SUM') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 2), (b'two', 3)]) def test_zinterstore_max(self): self.zadd('foo', {'one': 0}) self.zadd('foo', {'two': 0}) self.zadd('bar', {'one': 1}) self.zadd('bar', {'two': 2}) self.zadd('bar', {'three': 3}) self.redis.zinterstore('baz', ['foo', 'bar'], aggregate='MAX') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 1), (b'two', 2)]) def test_zinterstore_onekey(self): self.zadd('foo', {'one': 1}) self.redis.zinterstore('baz', ['foo'], aggregate='MAX') self.assertEqual(self.redis.zrange('baz', 0, -1, withscores=True), [(b'one', 1)]) def test_zinterstore_nokey(self): with self.assertRaises(redis.ResponseError): self.redis.zinterstore('baz', [], aggregate='MAX') def test_zinterstore_nan_to_zero(self): self.zadd('foo', {'x': math.inf}) self.zadd('foo2', {'x': math.inf}) self.redis.zinterstore('bar', OrderedDict([('foo', 1.0), ('foo2', 0.0)])) self.assertEqual(self.redis.zscore('bar', 'x'), 0.0) def test_zunionstore_nokey(self): with self.assertRaises(redis.ResponseError): self.redis.zunionstore('baz', [], aggregate='MAX') def test_zinterstore_wrong_type(self): self.redis.set('foo', 'bar') with self.assertRaises(redis.ResponseError): self.redis.zinterstore('baz', ['foo', 'bar']) def test_empty_zset(self): self.zadd('foo', {'one': 1}) self.redis.zrem('foo', 'one') self.assertFalse(self.redis.exists('foo')) def test_multidb(self): r1 = self.create_redis(db=0) r2 = self.create_redis(db=1) r1['r1'] = 'r1' r2['r2'] = 'r2' self.assertTrue('r2' not in r1) self.assertTrue('r1' not in r2) self.assertEqual(r1['r1'], b'r1') self.assertEqual(r2['r2'], b'r2') self.assertEqual(r1.flushall(), True) self.assertTrue('r1' not in r1) self.assertTrue('r2' not in r2) def test_basic_sort(self): self.redis.rpush('foo', '2') self.redis.rpush('foo', '1') self.redis.rpush('foo', '3') self.assertEqual(self.redis.sort('foo'), [b'1', b'2', b'3']) def test_empty_sort(self): self.assertEqual(self.redis.sort('foo'), []) def test_sort_range_offset_range(self): self.redis.rpush('foo', '2') self.redis.rpush('foo', '1') self.redis.rpush('foo', '4') self.redis.rpush('foo', '3') self.assertEqual(self.redis.sort('foo', start=0, num=2), [b'1', b'2']) def test_sort_range_offset_range_and_desc(self): self.redis.rpush('foo', '2') self.redis.rpush('foo', '1') self.redis.rpush('foo', '4') self.redis.rpush('foo', '3') self.assertEqual(self.redis.sort("foo", start=0, num=1, desc=True), [b"4"]) def test_sort_range_offset_norange(self): with self.assertRaises(redis.RedisError): self.redis.sort('foo', start=1) def test_sort_range_with_large_range(self): self.redis.rpush('foo', '2') self.redis.rpush('foo', '1') self.redis.rpush('foo', '4') self.redis.rpush('foo', '3') # num=20 even though len(foo) is 4. self.assertEqual(self.redis.sort('foo', start=1, num=20), [b'2', b'3', b'4']) def test_sort_descending(self): self.redis.rpush('foo', '1') self.redis.rpush('foo', '2') self.redis.rpush('foo', '3') self.assertEqual(self.redis.sort('foo', desc=True), [b'3', b'2', b'1']) def test_sort_alpha(self): self.redis.rpush('foo', '2a') self.redis.rpush('foo', '1b') self.redis.rpush('foo', '2b') self.redis.rpush('foo', '1a') self.assertEqual(self.redis.sort('foo', alpha=True), [b'1a', b'1b', b'2a', b'2b']) def test_sort_wrong_type(self): self.redis.set('string', '3') with self.assertRaises(redis.ResponseError): self.redis.sort('string') def test_foo(self): self.redis.rpush('foo', '2a') self.redis.rpush('foo', '1b') self.redis.rpush('foo', '2b') self.redis.rpush('foo', '1a') with self.assertRaises(redis.ResponseError): self.redis.sort('foo', alpha=False) def test_sort_with_store_option(self): self.redis.rpush('foo', '2') self.redis.rpush('foo', '1') self.redis.rpush('foo', '4') self.redis.rpush('foo', '3') self.assertEqual(self.redis.sort('foo', store='bar'), 4) self.assertEqual(self.redis.lrange('bar', 0, -1), [b'1', b'2', b'3', b'4']) def test_sort_with_by_and_get_option(self): self.redis.rpush('foo', '2') self.redis.rpush('foo', '1') self.redis.rpush('foo', '4') self.redis.rpush('foo', '3') self.redis['weight_1'] = '4' self.redis['weight_2'] = '3' self.redis['weight_3'] = '2' self.redis['weight_4'] = '1' self.redis['data_1'] = 'one' self.redis['data_2'] = 'two' self.redis['data_3'] = 'three' self.redis['data_4'] = 'four' self.assertEqual(self.redis.sort('foo', by='weight_*', get='data_*'), [b'four', b'three', b'two', b'one']) self.assertEqual(self.redis.sort('foo', by='weight_*', get='#'), [b'4', b'3', b'2', b'1']) self.assertEqual( self.redis.sort('foo', by='weight_*', get=('data_*', '#')), [b'four', b'4', b'three', b'3', b'two', b'2', b'one', b'1']) self.assertEqual(self.redis.sort('foo', by='weight_*', get='data_1'), [None, None, None, None]) def test_sort_with_hash(self): self.redis.rpush('foo', 'middle') self.redis.rpush('foo', 'eldest') self.redis.rpush('foo', 'youngest') self.redis.hset('record_youngest', 'age', 1) self.redis.hset('record_youngest', 'name', 'baby') self.redis.hset('record_middle', 'age', 10) self.redis.hset('record_middle', 'name', 'teen') self.redis.hset('record_eldest', 'age', 20) self.redis.hset('record_eldest', 'name', 'adult') self.assertEqual(self.redis.sort('foo', by='record_*->age'), [b'youngest', b'middle', b'eldest']) self.assertEqual( self.redis.sort('foo', by='record_*->age', get='record_*->name'), [b'baby', b'teen', b'adult']) def test_sort_with_set(self): self.redis.sadd('foo', '3') self.redis.sadd('foo', '1') self.redis.sadd('foo', '2') self.assertEqual(self.redis.sort('foo'), [b'1', b'2', b'3']) def test_pipeline(self): # The pipeline method returns an object for # issuing multiple commands in a batch. p = self.redis.pipeline() p.watch('bam') p.multi() p.set('foo', 'bar').get('foo') p.lpush('baz', 'quux') p.lpush('baz', 'quux2').lrange('baz', 0, -1) res = p.execute() # Check return values returned as list. self.assertEqual(res, [True, b'bar', 1, 2, [b'quux2', b'quux']]) # Check side effects happened as expected. self.assertEqual(self.redis.lrange('baz', 0, -1), [b'quux2', b'quux']) # Check that the command buffer has been emptied. self.assertEqual(p.execute(), []) def test_pipeline_ignore_errors(self): """Test the pipeline ignoring errors when asked.""" with self.redis.pipeline() as p: p.set('foo', 'bar') p.rename('baz', 'bats') with self.assertRaises(redis.exceptions.ResponseError): p.execute() self.assertEqual([], p.execute()) with self.redis.pipeline() as p: p.set('foo', 'bar') p.rename('baz', 'bats') res = p.execute(raise_on_error=False) self.assertEqual([], p.execute()) self.assertEqual(len(res), 2) self.assertIsInstance(res[1], redis.exceptions.ResponseError) def test_multiple_successful_watch_calls(self): p = self.redis.pipeline() p.watch('bam') p.multi() p.set('foo', 'bar') # Check that the watched keys buffer has been emptied. p.execute() # bam is no longer being watched, so it's ok to modify # it now. p.watch('foo') self.redis.set('bam', 'boo') p.multi() p.set('foo', 'bats') self.assertEqual(p.execute(), [True]) def test_pipeline_non_transactional(self): # For our simple-minded model I don't think # there is any observable difference. p = self.redis.pipeline(transaction=False) res = p.set('baz', 'quux').get('baz').execute() self.assertEqual(res, [True, b'quux']) def test_pipeline_raises_when_watched_key_changed(self): self.redis.set('foo', 'bar') self.redis.rpush('greet', 'hello') p = self.redis.pipeline() self.addCleanup(p.reset) p.watch('greet', 'foo') nextf = six.ensure_binary(p.get('foo')) + b'baz' # Simulate change happening on another thread. self.redis.rpush('greet', 'world') # Begin pipelining. p.multi() p.set('foo', nextf) with self.assertRaises(redis.WatchError): p.execute() def test_pipeline_succeeds_despite_unwatched_key_changed(self): # Same setup as before except for the params to the WATCH command. self.redis.set('foo', 'bar') self.redis.rpush('greet', 'hello') p = self.redis.pipeline() try: # Only watch one of the 2 keys. p.watch('foo') nextf = six.ensure_binary(p.get('foo')) + b'baz' # Simulate change happening on another thread. self.redis.rpush('greet', 'world') p.multi() p.set('foo', nextf) p.execute() # Check the commands were executed. self.assertEqual(self.redis.get('foo'), b'barbaz') finally: p.reset() def test_pipeline_succeeds_when_watching_nonexistent_key(self): self.redis.set('foo', 'bar') self.redis.rpush('greet', 'hello') p = self.redis.pipeline() try: # Also watch a nonexistent key. p.watch('foo', 'bam') nextf = six.ensure_binary(p.get('foo')) + b'baz' # Simulate change happening on another thread. self.redis.rpush('greet', 'world') p.multi() p.set('foo', nextf) p.execute() # Check the commands were executed. self.assertEqual(self.redis.get('foo'), b'barbaz') finally: p.reset() def test_watch_state_is_cleared_across_multiple_watches(self): self.redis.set('foo', 'one') self.redis.set('bar', 'baz') p = self.redis.pipeline() self.addCleanup(p.reset) p.watch('foo') # Simulate change happening on another thread. self.redis.set('foo', 'three') p.multi() p.set('foo', 'three') with self.assertRaises(redis.WatchError): p.execute() # Now watch another key. It should be ok to change # foo as we're no longer watching it. p.watch('bar') self.redis.set('foo', 'four') p.multi() p.set('bar', 'five') self.assertEqual(p.execute(), [True]) def test_pipeline_transaction_shortcut(self): # This example taken pretty much from the redis-py documentation. self.redis.set('OUR-SEQUENCE-KEY', 13) calls = [] def client_side_incr(pipe): calls.append((pipe,)) current_value = pipe.get('OUR-SEQUENCE-KEY') next_value = int(current_value) + 1 if len(calls) < 3: # Simulate a change from another thread. self.redis.set('OUR-SEQUENCE-KEY', next_value) pipe.multi() pipe.set('OUR-SEQUENCE-KEY', next_value) res = self.redis.transaction(client_side_incr, 'OUR-SEQUENCE-KEY') self.assertEqual([True], res) self.assertEqual(16, int(self.redis.get('OUR-SEQUENCE-KEY'))) self.assertEqual(3, len(calls)) def test_pipeline_transaction_value_from_callable(self): def callback(pipe): # No need to do anything here since we only want the return value return 'OUR-RETURN-VALUE' res = self.redis.transaction(callback, 'OUR-SEQUENCE-KEY', value_from_callable=True) self.assertEqual('OUR-RETURN-VALUE', res) def test_pipeline_empty(self): p = self.redis.pipeline() self.assertEqual(0, len(p)) def test_pipeline_length(self): p = self.redis.pipeline() p.set('baz', 'quux').get('baz') self.assertEqual(2, len(p)) def test_pipeline_no_commands(self): # Prior to 3.4, redis-py's execute is a nop if there are no commands # queued, so it succeeds even if watched keys have been changed. self.redis.set('foo', '1') p = self.redis.pipeline() p.watch('foo') self.redis.set('foo', '2') if REDIS_VERSION >= '3.4': with self.assertRaises(redis.WatchError): p.execute() else: self.assertEqual(p.execute(), []) def test_pipeline_failed_transaction(self): p = self.redis.pipeline() p.multi() p.set('foo', 'bar') # Deliberately induce a syntax error p.execute_command('set') # It should be an ExecAbortError, but redis-py tries to DISCARD after the # failed EXEC, which raises a ResponseError. with self.assertRaises(redis.ResponseError): p.execute() self.assertFalse(self.redis.exists('foo')) def test_key_patterns(self): self.redis.mset({'one': 1, 'two': 2, 'three': 3, 'four': 4}) self.assertItemsEqual(self.redis.keys('*o*'), [b'four', b'one', b'two']) self.assertItemsEqual(self.redis.keys('t??'), [b'two']) self.assertItemsEqual(self.redis.keys('*'), [b'four', b'one', b'two', b'three']) self.assertItemsEqual(self.redis.keys(), [b'four', b'one', b'two', b'three']) def test_ping(self): self.assertTrue(self.redis.ping()) self.assertEqual(self.raw_command('ping', 'test'), b'test') @redis3_only def test_ping_pubsub(self): p = self.redis.pubsub() p.subscribe('channel') p.parse_response() # Consume the subscribe reply p.ping() self.assertEqual(p.parse_response(), [b'pong', b'']) p.ping('test') self.assertEqual(p.parse_response(), [b'pong', b'test']) @redis3_only def test_swapdb(self): r1 = self.create_redis(1) self.redis.set('foo', 'abc') self.redis.set('bar', 'xyz') r1.set('foo', 'foo') r1.set('baz', 'baz') self.assertTrue(self.redis.swapdb(0, 1)) self.assertEqual(self.redis.get('foo'), b'foo') self.assertEqual(self.redis.get('bar'), None) self.assertEqual(self.redis.get('baz'), b'baz') self.assertEqual(r1.get('foo'), b'abc') self.assertEqual(r1.get('bar'), b'xyz') self.assertEqual(r1.get('baz'), None) @redis3_only def test_swapdb_same_db(self): self.assertTrue(self.redis.swapdb(1, 1)) def test_bgsave(self): self.assertTrue(self.redis.bgsave()) def test_save(self): self.assertTrue(self.redis.save()) def test_lastsave(self): self.assertTrue(isinstance(self.redis.lastsave(), datetime)) @pytest.mark.slow def test_bgsave_timestamp_update(self): early_timestamp = self.redis.lastsave() sleep(1) self.assertTrue(self.redis.bgsave()) sleep(1) late_timestamp = self.redis.lastsave() self.assertLess(early_timestamp, late_timestamp) @pytest.mark.slow def test_save_timestamp_update(self): early_timestamp = self.redis.lastsave() sleep(1) self.assertTrue(self.redis.save()) late_timestamp = self.redis.lastsave() self.assertLess(early_timestamp, late_timestamp) def test_type(self): self.redis.set('string_key', "value") self.redis.lpush("list_key", "value") self.redis.sadd("set_key", "value") self.zadd("zset_key", {"value": 1}) self.redis.hset('hset_key', 'key', 'value') self.assertEqual(self.redis.type('string_key'), b'string') self.assertEqual(self.redis.type('list_key'), b'list') self.assertEqual(self.redis.type('set_key'), b'set') self.assertEqual(self.redis.type('zset_key'), b'zset') self.assertEqual(self.redis.type('hset_key'), b'hash') self.assertEqual(self.redis.type('none_key'), b'none') @pytest.mark.slow def test_pubsub_subscribe(self): pubsub = self.redis.pubsub() pubsub.subscribe("channel") sleep(1) expected_message = {'type': 'subscribe', 'pattern': None, 'channel': b'channel', 'data': 1} message = pubsub.get_message() keys = list(pubsub.channels.keys()) key = keys[0] if not self.decode_responses: key = (key if type(key) == bytes else bytes(key, encoding='utf-8')) self.assertEqual(len(keys), 1) self.assertEqual(key, b'channel') self.assertEqual(message, expected_message) @pytest.mark.slow def test_pubsub_psubscribe(self): pubsub = self.redis.pubsub() pubsub.psubscribe("channel.*") sleep(1) expected_message = {'type': 'psubscribe', 'pattern': None, 'channel': b'channel.*', 'data': 1} message = pubsub.get_message() keys = list(pubsub.patterns.keys()) self.assertEqual(len(keys), 1) self.assertEqual(message, expected_message) @pytest.mark.slow def test_pubsub_unsubscribe(self): pubsub = self.redis.pubsub() pubsub.subscribe('channel-1', 'channel-2', 'channel-3') sleep(1) expected_message = {'type': 'unsubscribe', 'pattern': None, 'channel': b'channel-1', 'data': 2} pubsub.get_message() pubsub.get_message() pubsub.get_message() # unsubscribe from one pubsub.unsubscribe('channel-1') sleep(1) message = pubsub.get_message() keys = list(pubsub.channels.keys()) self.assertEqual(message, expected_message) self.assertEqual(len(keys), 2) # unsubscribe from multiple pubsub.unsubscribe() sleep(1) pubsub.get_message() pubsub.get_message() keys = list(pubsub.channels.keys()) self.assertEqual(message, expected_message) self.assertEqual(len(keys), 0) @pytest.mark.slow def test_pubsub_punsubscribe(self): pubsub = self.redis.pubsub() pubsub.psubscribe('channel-1.*', 'channel-2.*', 'channel-3.*') sleep(1) expected_message = {'type': 'punsubscribe', 'pattern': None, 'channel': b'channel-1.*', 'data': 2} pubsub.get_message() pubsub.get_message() pubsub.get_message() # unsubscribe from one pubsub.punsubscribe('channel-1.*') sleep(1) message = pubsub.get_message() keys = list(pubsub.patterns.keys()) self.assertEqual(message, expected_message) self.assertEqual(len(keys), 2) # unsubscribe from multiple pubsub.punsubscribe() sleep(1) pubsub.get_message() pubsub.get_message() keys = list(pubsub.patterns.keys()) self.assertEqual(len(keys), 0) @pytest.mark.slow def test_pubsub_listen(self): def _listen(pubsub, q): count = 0 for message in pubsub.listen(): q.put(message) count += 1 if count == 4: pubsub.close() channel = 'ch1' patterns = ['ch1*', 'ch[1]', 'ch?'] pubsub = self.redis.pubsub() pubsub.subscribe(channel) pubsub.psubscribe(*patterns) sleep(1) msg1 = pubsub.get_message() msg2 = pubsub.get_message() msg3 = pubsub.get_message() msg4 = pubsub.get_message() self.assertEqual(msg1['type'], 'subscribe') self.assertEqual(msg2['type'], 'psubscribe') self.assertEqual(msg3['type'], 'psubscribe') self.assertEqual(msg4['type'], 'psubscribe') q = Queue() t = threading.Thread(target=_listen, args=(pubsub, q)) t.start() msg = 'hello world' self.redis.publish(channel, msg) t.join() msg1 = q.get() msg2 = q.get() msg3 = q.get() msg4 = q.get() if self.decode_responses: bpatterns = patterns + [channel] else: bpatterns = [pattern.encode() for pattern in patterns] bpatterns.append(channel.encode()) msg = msg.encode() self.assertEqual(msg1['data'], msg) self.assertIn(msg1['channel'], bpatterns) self.assertEqual(msg2['data'], msg) self.assertIn(msg2['channel'], bpatterns) self.assertEqual(msg3['data'], msg) self.assertIn(msg3['channel'], bpatterns) self.assertEqual(msg4['data'], msg) self.assertIn(msg4['channel'], bpatterns) @pytest.mark.slow def test_pubsub_listen_handler(self): def _handler(message): calls.append(message) channel = 'ch1' patterns = {'ch?': _handler} calls = [] pubsub = self.redis.pubsub() pubsub.subscribe(ch1=_handler) pubsub.psubscribe(**patterns) sleep(1) msg1 = pubsub.get_message() msg2 = pubsub.get_message() self.assertEqual(msg1['type'], 'subscribe') self.assertEqual(msg2['type'], 'psubscribe') msg = 'hello world' self.redis.publish(channel, msg) sleep(1) for i in range(2): msg = pubsub.get_message() self.assertIsNone(msg) # get_message returns None when handler is used pubsub.close() calls.sort(key=lambda call: call['type']) self.assertEqual(calls, [ {'pattern': None, 'channel': b'ch1', 'data': b'hello world', 'type': 'message'}, {'pattern': b'ch?', 'channel': b'ch1', 'data': b'hello world', 'type': 'pmessage'} ]) @pytest.mark.slow def test_pubsub_ignore_sub_messages_listen(self): def _listen(pubsub, q): count = 0 for message in pubsub.listen(): q.put(message) count += 1 if count == 4: pubsub.close() channel = 'ch1' patterns = ['ch1*', 'ch[1]', 'ch?'] pubsub = self.redis.pubsub(ignore_subscribe_messages=True) pubsub.subscribe(channel) pubsub.psubscribe(*patterns) sleep(1) q = Queue() t = threading.Thread(target=_listen, args=(pubsub, q)) t.start() msg = 'hello world' self.redis.publish(channel, msg) t.join() msg1 = q.get() msg2 = q.get() msg3 = q.get() msg4 = q.get() if self.decode_responses: bpatterns = patterns + [channel] else: bpatterns = [pattern.encode() for pattern in patterns] bpatterns.append(channel.encode()) msg = msg.encode() self.assertEqual(msg1['data'], msg) self.assertIn(msg1['channel'], bpatterns) self.assertEqual(msg2['data'], msg) self.assertIn(msg2['channel'], bpatterns) self.assertEqual(msg3['data'], msg) self.assertIn(msg3['channel'], bpatterns) self.assertEqual(msg4['data'], msg) self.assertIn(msg4['channel'], bpatterns) @pytest.mark.slow def test_pubsub_binary(self): if self.decode_responses: # Reading the non-UTF-8 message will break if decoding # responses. return def _listen(pubsub, q): for message in pubsub.listen(): q.put(message) pubsub.close() pubsub = self.redis.pubsub(ignore_subscribe_messages=True) pubsub.subscribe('channel\r\n\xff') sleep(1) q = Queue() t = threading.Thread(target=_listen, args=(pubsub, q)) t.start() msg = b'\x00hello world\r\n\xff' self.redis.publish('channel\r\n\xff', msg) t.join() received = q.get() self.assertEqual(received['data'], msg) @pytest.mark.slow def test_pubsub_run_in_thread(self): q = Queue() pubsub = self.redis.pubsub() pubsub.subscribe(channel=q.put) pubsub_thread = pubsub.run_in_thread() msg = b"Hello World" self.redis.publish("channel", msg) retrieved = q.get() self.assertEqual(retrieved["data"], msg) pubsub_thread.stop() # Newer versions of redis wait for an unsubscribe message, which sometimes comes early # https://github.com/andymccurdy/redis-py/issues/1150 if pubsub.channels: pubsub.channels = {} pubsub_thread.join() self.assertTrue(not pubsub_thread.is_alive()) pubsub.subscribe(channel=None) with self.assertRaises(redis.exceptions.PubSubError): pubsub_thread = pubsub.run_in_thread() pubsub.unsubscribe("channel") pubsub.psubscribe(channel=None) with self.assertRaises(redis.exceptions.PubSubError): pubsub_thread = pubsub.run_in_thread() @pytest.mark.slow def test_pubsub_timeout(self): def publish(): sleep(0.1) self.redis.publish('channel', 'hello') p = self.redis.pubsub() p.subscribe('channel') p.parse_response() # Drains the subscribe message publish_thread = threading.Thread(target=publish) publish_thread.start() message = p.get_message(timeout=1) self.assertEqual(message, {'type': 'message', 'pattern': None, 'channel': b'channel', 'data': b'hello'}) publish_thread.join() message = p.get_message(timeout=0.5) self.assertIsNone(message) def test_pfadd(self): key = "hll-pfadd" self.assertEqual( 1, self.redis.pfadd(key, "a", "b", "c", "d", "e", "f", "g")) self.assertEqual(7, self.redis.pfcount(key)) def test_pfcount(self): key1 = "hll-pfcount01" key2 = "hll-pfcount02" key3 = "hll-pfcount03" self.assertEqual(1, self.redis.pfadd(key1, "foo", "bar", "zap")) self.assertEqual(0, self.redis.pfadd(key1, "zap", "zap", "zap")) self.assertEqual(0, self.redis.pfadd(key1, "foo", "bar")) self.assertEqual(3, self.redis.pfcount(key1)) self.assertEqual(1, self.redis.pfadd(key2, "1", "2", "3")) self.assertEqual(3, self.redis.pfcount(key2)) self.assertEqual(6, self.redis.pfcount(key1, key2)) self.assertEqual(1, self.redis.pfadd(key3, "foo", "bar", "zip")) self.assertEqual(3, self.redis.pfcount(key3)) self.assertEqual(4, self.redis.pfcount(key1, key3)) self.assertEqual(7, self.redis.pfcount(key1, key2, key3)) def test_pfmerge(self): key1 = "hll-pfmerge01" key2 = "hll-pfmerge02" key3 = "hll-pfmerge03" self.assertEqual(1, self.redis.pfadd(key1, "foo", "bar", "zap", "a")) self.assertEqual(1, self.redis.pfadd(key2, "a", "b", "c", "foo")) self.assertTrue(self.redis.pfmerge(key3, key1, key2)) self.assertEqual(6, self.redis.pfcount(key3)) def test_scan(self): # Setup the data for ix in range(20): k = 'scan-test:%s' % ix v = 'result:%s' % ix self.redis.set(k, v) expected = self.redis.keys() self.assertEqual(20, len(expected)) # Ensure we know what we're testing # Test that we page through the results and get everything out results = [] cursor = '0' while cursor != 0: cursor, data = self.redis.scan(cursor, count=6) results.extend(data) self.assertSetEqual(set(expected), set(results)) # Now test that the MATCH functionality works results = [] cursor = '0' while cursor != 0: cursor, data = self.redis.scan(cursor, match='*7', count=100) results.extend(data) self.assertIn(b'scan-test:7', results) self.assertIn(b'scan-test:17', results) self.assertEqual(2, len(results)) # Test the match on iterator results = [r for r in self.redis.scan_iter(match='*7')] self.assertIn(b'scan-test:7', results) self.assertIn(b'scan-test:17', results) self.assertEqual(2, len(results)) def test_sscan(self): # Setup the data name = 'sscan-test' for ix in range(20): k = 'sscan-test:%s' % ix self.redis.sadd(name, k) expected = self.redis.smembers(name) self.assertEqual(20, len(expected)) # Ensure we know what we're testing # Test that we page through the results and get everything out results = [] cursor = '0' while cursor != 0: cursor, data = self.redis.sscan(name, cursor, count=6) results.extend(data) self.assertSetEqual(set(expected), set(results)) # Test the iterator version results = [r for r in self.redis.sscan_iter(name, count=6)] self.assertSetEqual(set(expected), set(results)) # Now test that the MATCH functionality works results = [] cursor = '0' while cursor != 0: cursor, data = self.redis.sscan(name, cursor, match='*7', count=100) results.extend(data) self.assertIn(b'sscan-test:7', results) self.assertIn(b'sscan-test:17', results) self.assertEqual(2, len(results)) # Test the match on iterator results = [r for r in self.redis.sscan_iter(name, match='*7')] self.assertIn(b'sscan-test:7', results) self.assertIn(b'sscan-test:17', results) self.assertEqual(2, len(results)) def test_hscan(self): # Setup the data name = 'hscan-test' for ix in range(20): k = 'key:%s' % ix v = 'result:%s' % ix self.redis.hset(name, k, v) expected = self.redis.hgetall(name) self.assertEqual(20, len(expected)) # Ensure we know what we're testing # Test that we page through the results and get everything out results = {} cursor = '0' while cursor != 0: cursor, data = self.redis.hscan(name, cursor, count=6) results.update(data) self.assertDictEqual(expected, results) # Test the iterator version results = {} for key, val in self.redis.hscan_iter(name, count=6): results[key] = val self.assertDictEqual(expected, results) # Now test that the MATCH functionality works results = {} cursor = '0' while cursor != 0: cursor, data = self.redis.hscan(name, cursor, match='*7', count=100) results.update(data) self.assertIn(b'key:7', results) self.assertIn(b'key:17', results) self.assertEqual(2, len(results)) # Test the match on iterator results = {} for key, val in self.redis.hscan_iter(name, match='*7'): results[key] = val self.assertIn(b'key:7', results) self.assertIn(b'key:17', results) self.assertEqual(2, len(results)) def test_zscan(self): # Setup the data name = 'zscan-test' for ix in range(20): self.zadd(name, {'key:%s' % ix: ix}) expected = dict(self.redis.zrange(name, 0, -1, withscores=True)) # Test the basic version results = {} for key, val in self.redis.zscan_iter(name, count=6): results[key] = val self.assertEqual(expected, results) # Now test that the MATCH functionality works results = {} cursor = '0' while cursor != 0: cursor, data = self.redis.zscan(name, cursor, match='*7', count=6) results.update(data) self.assertEqual(results, {b'key:7': 7.0, b'key:17': 17.0}) @pytest.mark.slow def test_set_ex_should_expire_value(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.set('foo', 'bar', ex=1) sleep(2) self.assertEqual(self.redis.get('foo'), None) @pytest.mark.slow def test_set_px_should_expire_value(self): self.redis.set('foo', 'bar', px=500) sleep(1.5) self.assertEqual(self.redis.get('foo'), None) @pytest.mark.slow def test_psetex_expire_value(self): with self.assertRaises(ResponseError): self.redis.psetex('foo', 0, 'bar') self.redis.psetex('foo', 500, 'bar') sleep(1.5) self.assertEqual(self.redis.get('foo'), None) @pytest.mark.slow def test_psetex_expire_value_using_timedelta(self): with self.assertRaises(ResponseError): self.redis.psetex('foo', timedelta(seconds=0), 'bar') self.redis.psetex('foo', timedelta(seconds=0.5), 'bar') sleep(1.5) self.assertEqual(self.redis.get('foo'), None) @pytest.mark.slow def test_expire_should_expire_key(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.expire('foo', 1) sleep(1.5) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.expire('bar', 1), False) def test_expire_should_return_true_for_existing_key(self): self.redis.set('foo', 'bar') rv = self.redis.expire('foo', 1) self.assertIs(rv, True) def test_expire_should_return_false_for_missing_key(self): rv = self.redis.expire('missing', 1) self.assertIs(rv, False) @pytest.mark.slow def test_expire_should_expire_key_using_timedelta(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.expire('foo', timedelta(seconds=1)) sleep(1.5) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.expire('bar', 1), False) @pytest.mark.slow def test_expire_should_expire_immediately_with_millisecond_timedelta(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.expire('foo', timedelta(milliseconds=750)) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.expire('bar', 1), False) @pytest.mark.slow def test_pexpire_should_expire_key(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.pexpire('foo', 150) sleep(0.2) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.pexpire('bar', 1), False) def test_pexpire_should_return_truthy_for_existing_key(self): self.redis.set('foo', 'bar') rv = self.redis.pexpire('foo', 1) self.assertIs(bool(rv), True) def test_pexpire_should_return_falsey_for_missing_key(self): rv = self.redis.pexpire('missing', 1) self.assertIs(bool(rv), False) @pytest.mark.slow def test_pexpire_should_expire_key_using_timedelta(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.pexpire('foo', timedelta(milliseconds=750)) sleep(0.5) self.assertEqual(self.redis.get('foo'), b'bar') sleep(0.5) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.pexpire('bar', 1), False) @pytest.mark.slow def test_expireat_should_expire_key_by_datetime(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.expireat('foo', datetime.now() + timedelta(seconds=1)) sleep(1.5) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.expireat('bar', datetime.now()), False) @pytest.mark.slow def test_expireat_should_expire_key_by_timestamp(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.expireat('foo', int(time() + 1)) sleep(1.5) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.expire('bar', 1), False) def test_expireat_should_return_true_for_existing_key(self): self.redis.set('foo', 'bar') rv = self.redis.expireat('foo', int(time() + 1)) self.assertIs(rv, True) def test_expireat_should_return_false_for_missing_key(self): rv = self.redis.expireat('missing', int(time() + 1)) self.assertIs(rv, False) @pytest.mark.slow def test_pexpireat_should_expire_key_by_datetime(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.pexpireat('foo', datetime.now() + timedelta(milliseconds=150)) sleep(0.2) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.pexpireat('bar', datetime.now()), False) @pytest.mark.slow def test_pexpireat_should_expire_key_by_timestamp(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.redis.pexpireat('foo', int(time() * 1000 + 150)) sleep(0.2) self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.expire('bar', 1), False) def test_pexpireat_should_return_true_for_existing_key(self): self.redis.set('foo', 'bar') rv = self.redis.pexpireat('foo', int(time() * 1000 + 150)) self.assertIs(bool(rv), True) def test_pexpireat_should_return_false_for_missing_key(self): rv = self.redis.pexpireat('missing', int(time() * 1000 + 150)) self.assertIs(bool(rv), False) def test_expire_should_not_handle_floating_point_values(self): self.redis.set('foo', 'bar') with self.assertRaisesRegex( redis.ResponseError, 'value is not an integer or out of range'): self.redis.expire('something_new', 1.2) self.redis.pexpire('something_new', 1000.2) self.redis.expire('some_unused_key', 1.2) self.redis.pexpire('some_unused_key', 1000.2) def test_ttl_should_return_minus_one_for_non_expiring_key(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.ttl('foo'), -1) def test_ttl_should_return_minus_two_for_non_existent_key(self): self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.ttl('foo'), -2) def test_pttl_should_return_minus_one_for_non_expiring_key(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.pttl('foo'), -1) def test_pttl_should_return_minus_two_for_non_existent_key(self): self.assertEqual(self.redis.get('foo'), None) self.assertEqual(self.redis.pttl('foo'), -2) def test_persist(self): self.redis.set('foo', 'bar', ex=20) self.assertEqual(self.redis.persist('foo'), 1) self.assertEqual(self.redis.ttl('foo'), -1) self.assertEqual(self.redis.persist('foo'), 0) def test_set_existing_key_persists(self): self.redis.set('foo', 'bar', ex=20) self.redis.set('foo', 'foo') self.assertEqual(self.redis.ttl('foo'), -1) def test_eval_set_value_to_arg(self): self.redis.eval('redis.call("SET", KEYS[1], ARGV[1])', 1, 'foo', 'bar') val = self.redis.get('foo') self.assertEqual(val, b'bar') def test_eval_conditional(self): lua = """ local val = redis.call("GET", KEYS[1]) if val == ARGV[1] then redis.call("SET", KEYS[1], ARGV[2]) else redis.call("SET", KEYS[1], ARGV[1]) end """ self.redis.eval(lua, 1, 'foo', 'bar', 'baz') val = self.redis.get('foo') self.assertEqual(val, b'bar') self.redis.eval(lua, 1, 'foo', 'bar', 'baz') val = self.redis.get('foo') self.assertEqual(val, b'baz') def test_eval_table(self): lua = """ local a = {} a[1] = "foo" a[2] = "bar" a[17] = "baz" return a """ val = self.redis.eval(lua, 0) self.assertEqual(val, [b'foo', b'bar']) def test_eval_table_with_nil(self): lua = """ local a = {} a[1] = "foo" a[2] = nil a[3] = "bar" return a """ val = self.redis.eval(lua, 0) self.assertEqual(val, [b'foo']) def test_eval_table_with_numbers(self): lua = """ local a = {} a[1] = 42 return a """ val = self.redis.eval(lua, 0) self.assertEqual(val, [42]) def test_eval_nested_table(self): lua = """ local a = {} a[1] = {} a[1][1] = "foo" return a """ val = self.redis.eval(lua, 0) self.assertEqual(val, [[b'foo']]) def test_eval_iterate_over_argv(self): lua = """ for i, v in ipairs(ARGV) do end return ARGV """ val = self.redis.eval(lua, 0, "a", "b", "c") self.assertEqual(val, [b"a", b"b", b"c"]) def test_eval_iterate_over_keys(self): lua = """ for i, v in ipairs(KEYS) do end return KEYS """ val = self.redis.eval(lua, 2, "a", "b", "c") self.assertEqual(val, [b"a", b"b"]) def test_eval_mget(self): self.redis.set('foo1', 'bar1') self.redis.set('foo2', 'bar2') val = self.redis.eval('return redis.call("mget", "foo1", "foo2")', 2, 'foo1', 'foo2') self.assertEqual(val, [b'bar1', b'bar2']) @redis2_only def test_eval_mget_none(self): self.redis.set('foo1', None) self.redis.set('foo2', None) val = self.redis.eval('return redis.call("mget", "foo1", "foo2")', 2, 'foo1', 'foo2') self.assertEqual(val, [b'None', b'None']) def test_eval_mget_not_set(self): val = self.redis.eval('return redis.call("mget", "foo1", "foo2")', 2, 'foo1', 'foo2') self.assertEqual(val, [None, None]) def test_eval_hgetall(self): self.redis.hset('foo', 'k1', 'bar') self.redis.hset('foo', 'k2', 'baz') val = self.redis.eval('return redis.call("hgetall", "foo")', 1, 'foo') sorted_val = sorted([val[:2], val[2:]]) self.assertEqual( sorted_val, [[b'k1', b'bar'], [b'k2', b'baz']] ) def test_eval_hgetall_iterate(self): self.redis.hset('foo', 'k1', 'bar') self.redis.hset('foo', 'k2', 'baz') lua = """ local result = redis.call("hgetall", "foo") for i, v in ipairs(result) do end return result """ val = self.redis.eval(lua, 1, 'foo') sorted_val = sorted([val[:2], val[2:]]) self.assertEqual( sorted_val, [[b'k1', b'bar'], [b'k2', b'baz']] ) @redis2_only def test_eval_list_with_nil(self): self.redis.lpush('foo', 'bar') self.redis.lpush('foo', None) self.redis.lpush('foo', 'baz') val = self.redis.eval('return redis.call("lrange", KEYS[1], 0, 2)', 1, 'foo') self.assertEqual(val, [b'baz', b'None', b'bar']) def test_eval_invalid_command(self): with self.assertRaises(ResponseError): self.redis.eval( 'return redis.call("FOO")', 0 ) def test_eval_syntax_error(self): with self.assertRaises(ResponseError): self.redis.eval('return "', 0) def test_eval_runtime_error(self): with self.assertRaises(ResponseError): self.redis.eval('error("CRASH")', 0) def test_eval_more_keys_than_args(self): with self.assertRaises(ResponseError): self.redis.eval('return 1', 42) def test_eval_numkeys_float_string(self): with self.assertRaises(ResponseError): self.redis.eval('return KEYS[1]', '0.7', 'foo') def test_eval_numkeys_integer_string(self): val = self.redis.eval('return KEYS[1]', "1", "foo") self.assertEqual(val, b'foo') def test_eval_numkeys_negative(self): with self.assertRaises(ResponseError): self.redis.eval('return KEYS[1]', -1, "foo") def test_eval_numkeys_float(self): with self.assertRaises(ResponseError): self.redis.eval('return KEYS[1]', 0.7, "foo") def test_eval_global_variable(self): # Redis doesn't allow script to define global variables with self.assertRaises(ResponseError): self.redis.eval('a=10', 0) def test_eval_global_and_return_ok(self): # Redis doesn't allow script to define global variables with self.assertRaises(ResponseError): self.redis.eval( ''' a=10 return redis.status_reply("Everything is awesome") ''', 0 ) def test_eval_convert_number(self): # Redis forces all Lua numbers to integer val = self.redis.eval('return 3.2', 0) self.assertEqual(val, 3) val = self.redis.eval('return 3.8', 0) self.assertEqual(val, 3) val = self.redis.eval('return -3.8', 0) self.assertEqual(val, -3) def test_eval_convert_bool(self): # Redis converts true to 1 and false to nil (which redis-py converts to None) val = self.redis.eval('return false', 0) self.assertIsNone(val) val = self.redis.eval('return true', 0) self.assertEqual(val, 1) self.assertNotIsInstance(val, bool) @redis2_only def test_eval_none_arg(self): val = self.redis.eval('return ARGV[1] == "None"', 0, None) self.assertTrue(val) def test_eval_return_error(self): with self.assertRaises(redis.ResponseError) as cm: self.redis.eval('return {err="Testing"}', 0) self.assertIn('Testing', str(cm.exception)) with self.assertRaises(redis.ResponseError) as cm: self.redis.eval('return redis.error_reply("Testing")', 0) self.assertIn('Testing', str(cm.exception)) def test_eval_return_ok(self): val = self.redis.eval('return {ok="Testing"}', 0) self.assertEqual(val, b'Testing') val = self.redis.eval('return redis.status_reply("Testing")', 0) self.assertEqual(val, b'Testing') def test_eval_return_ok_nested(self): val = self.redis.eval( ''' local a = {} a[1] = {ok="Testing"} return a ''', 0 ) self.assertEqual(val, [b'Testing']) def test_eval_return_ok_wrong_type(self): with self.assertRaises(redis.ResponseError): self.redis.eval('return redis.status_reply(123)', 0) def test_eval_pcall(self): val = self.redis.eval( ''' local a = {} a[1] = redis.pcall("foo") return a ''', 0 ) self.assertIsInstance(val, list) self.assertEqual(len(val), 1) self.assertIsInstance(val[0], ResponseError) def test_eval_pcall_return_value(self): with self.assertRaises(ResponseError): self.redis.eval('return redis.pcall("foo")', 0) def test_eval_delete(self): self.redis.set('foo', 'bar') val = self.redis.get('foo') self.assertEqual(val, b'bar') val = self.redis.eval('redis.call("DEL", KEYS[1])', 1, 'foo') self.assertIsNone(val) def test_eval_exists(self): val = self.redis.eval('return redis.call("exists", KEYS[1]) == 0', 1, 'foo') self.assertEqual(val, 1) def test_eval_flushdb(self): self.redis.set('foo', 'bar') val = self.redis.eval( ''' local value = redis.call("FLUSHDB"); return type(value) == "table" and value.ok == "OK"; ''', 0 ) self.assertEqual(val, 1) def test_eval_flushall(self): r1 = self.create_redis(db=0) r2 = self.create_redis(db=1) r1['r1'] = 'r1' r2['r2'] = 'r2' val = self.redis.eval( ''' local value = redis.call("FLUSHALL"); return type(value) == "table" and value.ok == "OK"; ''', 0 ) self.assertEqual(val, 1) self.assertNotIn('r1', r1) self.assertNotIn('r2', r2) def test_eval_incrbyfloat(self): self.redis.set('foo', 0.5) val = self.redis.eval( ''' local value = redis.call("INCRBYFLOAT", KEYS[1], 2.0); return type(value) == "string" and tonumber(value) == 2.5; ''', 1, 'foo' ) self.assertEqual(val, 1) def test_eval_lrange(self): self.redis.rpush('foo', 'a', 'b') val = self.redis.eval( ''' local value = redis.call("LRANGE", KEYS[1], 0, -1); return type(value) == "table" and value[1] == "a" and value[2] == "b"; ''', 1, 'foo' ) self.assertEqual(val, 1) def test_eval_ltrim(self): self.redis.rpush('foo', 'a', 'b', 'c', 'd') val = self.redis.eval( ''' local value = redis.call("LTRIM", KEYS[1], 1, 2); return type(value) == "table" and value.ok == "OK"; ''', 1, 'foo' ) self.assertEqual(val, 1) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'b', b'c']) def test_eval_lset(self): self.redis.rpush('foo', 'a', 'b') val = self.redis.eval( ''' local value = redis.call("LSET", KEYS[1], 0, "z"); return type(value) == "table" and value.ok == "OK"; ''', 1, 'foo' ) self.assertEqual(val, 1) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'z', b'b']) def test_eval_sdiff(self): self.redis.sadd('foo', 'a', 'b', 'c', 'f', 'e', 'd') self.redis.sadd('bar', 'b') val = self.redis.eval( ''' local value = redis.call("SDIFF", KEYS[1], KEYS[2]); if type(value) ~= "table" then return redis.error_reply(type(value) .. ", should be table"); else return value; end ''', 2, 'foo', 'bar') # Note: while fakeredis sorts the result when using Lua, this isn't # actually part of the redis contract (see # https://github.com/antirez/redis/issues/5538), and for Redis 5 we # need to sort val to pass the test. self.assertEqual(sorted(val), [b'a', b'c', b'd', b'e', b'f']) def test_script(self): script = self.redis.register_script('return ARGV[1]') result = script(args=[42]) self.assertEqual(result, b'42') @redis3_only def test_unlink(self): self.redis.set('foo', 'bar') self.redis.unlink('foo') self.assertIsNone(self.redis.get('foo')) @redis2_only class TestFakeRedis(unittest.TestCase): decode_responses = False def setUp(self): self.server = fakeredis.FakeServer() self.redis = self.create_redis() def tearDown(self): self.redis.flushall() del self.redis def assertInRange(self, value, start, end, msg=None): self.assertGreaterEqual(value, start, msg) self.assertLessEqual(value, end, msg) def create_redis(self, db=0): return fakeredis.FakeRedis(db=db, server=self.server) def test_setex(self): self.assertEqual(self.redis.setex('foo', 'bar', 100), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_setex_using_timedelta(self): self.assertEqual( self.redis.setex('foo', 'bar', timedelta(seconds=100)), True) self.assertEqual(self.redis.get('foo'), b'bar') def test_lrem_positive_count(self): self.redis.lpush('foo', 'same') self.redis.lpush('foo', 'same') self.redis.lpush('foo', 'different') self.redis.lrem('foo', 'same', 2) self.assertEqual(self.redis.lrange('foo', 0, -1), [b'different']) def test_lrem_negative_count(self): self.redis.lpush('foo', 'removeme') self.redis.lpush('foo', 'three') self.redis.lpush('foo', 'two') self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'removeme') self.redis.lrem('foo', 'removeme', -1) # Should remove it from the end of the list, # leaving the 'removeme' from the front of the list alone. self.assertEqual(self.redis.lrange('foo', 0, -1), [b'removeme', b'one', b'two', b'three']) def test_lrem_zero_count(self): self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lrem('foo', 'one') self.assertEqual(self.redis.lrange('foo', 0, -1), []) def test_lrem_default_value(self): self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lpush('foo', 'one') self.redis.lrem('foo', 'one') self.assertEqual(self.redis.lrange('foo', 0, -1), []) def test_lrem_does_not_exist(self): self.redis.lpush('foo', 'one') self.redis.lrem('foo', 'one') # These should be noops. self.redis.lrem('foo', 'one', -2) self.redis.lrem('foo', 'one', 2) def test_lrem_return_value(self): self.redis.lpush('foo', 'one') count = self.redis.lrem('foo', 'one', 0) self.assertEqual(count, 1) self.assertEqual(self.redis.lrem('foo', 'one'), 0) def test_zadd_deprecated(self): result = self.redis.zadd('foo', 'one', 1) self.assertEqual(result, 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'one']) def test_zadd_missing_required_params(self): with self.assertRaises(redis.RedisError): # Missing the 'score' param. self.redis.zadd('foo', 'one') with self.assertRaises(redis.RedisError): # Missing the 'value' param. self.redis.zadd('foo', None, score=1) with self.assertRaises(redis.RedisError): self.redis.zadd('foo') def test_zadd_with_single_keypair(self): result = self.redis.zadd('foo', bar=1) self.assertEqual(result, 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'bar']) def test_zadd_with_multiple_keypairs(self): result = self.redis.zadd('foo', bar=1, baz=9) self.assertEqual(result, 2) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'bar', b'baz']) def test_zadd_with_name_is_non_string(self): result = self.redis.zadd('foo', 1, 9) self.assertEqual(result, 1) self.assertEqual(self.redis.zrange('foo', 0, -1), [b'1']) def test_ttl_should_return_none_for_non_expiring_key(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.ttl('foo'), None) def test_ttl_should_return_value_for_expiring_key(self): self.redis.set('foo', 'bar') self.redis.expire('foo', 1) self.assertEqual(self.redis.ttl('foo'), 1) self.redis.expire('foo', 2) self.assertEqual(self.redis.ttl('foo'), 2) # See https://github.com/antirez/redis/blob/unstable/src/db.c#L632 ttl = 1000000000 self.redis.expire('foo', ttl) self.assertEqual(self.redis.ttl('foo'), ttl) def test_pttl_should_return_none_for_non_expiring_key(self): self.redis.set('foo', 'bar') self.assertEqual(self.redis.get('foo'), b'bar') self.assertEqual(self.redis.pttl('foo'), None) def test_pttl_should_return_value_for_expiring_key(self): d = 100 self.redis.set('foo', 'bar') self.redis.expire('foo', 1) self.assertInRange(self.redis.pttl('foo'), 1000 - d, 1000) self.redis.expire('foo', 2) self.assertInRange(self.redis.pttl('foo'), 2000 - d, 2000) ttl = 1000000000 # See https://github.com/antirez/redis/blob/unstable/src/db.c#L632 self.redis.expire('foo', ttl) self.assertInRange(self.redis.pttl('foo'), ttl * 1000 - d, ttl * 1000) def test_expire_should_not_handle_floating_point_values(self): self.redis.set('foo', 'bar') with self.assertRaisesRegex( redis.ResponseError, 'value is not an integer or out of range'): self.redis.expire('something_new', 1.2) self.redis.pexpire('something_new', 1000.2) self.redis.expire('some_unused_key', 1.2) self.redis.pexpire('some_unused_key', 1000.2) def test_lock(self): lock = self.redis.lock('foo') self.assertTrue(lock.acquire()) self.assertTrue(self.redis.exists('foo')) lock.release() self.assertFalse(self.redis.exists('foo')) with self.redis.lock('bar'): self.assertTrue(self.redis.exists('bar')) self.assertFalse(self.redis.exists('bar')) def test_unlock_without_lock(self): lock = self.redis.lock('foo') with self.assertRaises(redis.exceptions.LockError): lock.release() @pytest.mark.slow def test_unlock_expired(self): lock = self.redis.lock('foo', timeout=0.01, sleep=0.001) self.assertTrue(lock.acquire()) sleep(0.1) with self.assertRaises(redis.exceptions.LockError): lock.release() @pytest.mark.slow def test_lock_blocking_timeout(self): lock = self.redis.lock('foo') self.assertTrue(lock.acquire()) lock2 = self.redis.lock('foo') self.assertFalse(lock2.acquire(blocking_timeout=1)) def test_lock_nonblocking(self): lock = self.redis.lock('foo') self.assertTrue(lock.acquire()) lock2 = self.redis.lock('foo') self.assertFalse(lock2.acquire(blocking=False)) def test_lock_twice(self): lock = self.redis.lock('foo') self.assertTrue(lock.acquire(blocking=False)) self.assertFalse(lock.acquire(blocking=False)) def test_acquiring_lock_different_lock_release(self): lock1 = self.redis.lock('foo') lock2 = self.redis.lock('foo') self.assertTrue(lock1.acquire(blocking=False)) self.assertFalse(lock2.acquire(blocking=False)) # Test only releasing lock1 actually releases the lock with self.assertRaises(redis.exceptions.LockError): lock2.release() self.assertFalse(lock2.acquire(blocking=False)) lock1.release() # Locking with lock2 now has the lock self.assertTrue(lock2.acquire(blocking=False)) self.assertFalse(lock1.acquire(blocking=False)) def test_lock_extend(self): lock = self.redis.lock('foo', timeout=2) lock.acquire() lock.extend(3) ttl = int(self.redis.pttl('foo')) self.assertGreater(ttl, 4000) self.assertLessEqual(ttl, 5000) def test_lock_extend_exceptions(self): lock1 = self.redis.lock('foo', timeout=2) with self.assertRaises(redis.exceptions.LockError): lock1.extend(3) lock2 = self.redis.lock('foo') lock2.acquire() with self.assertRaises(redis.exceptions.LockError): lock2.extend(3) # Cannot extend a lock with no timeout @pytest.mark.slow def test_lock_extend_expired(self): lock = self.redis.lock('foo', timeout=0.01, sleep=0.001) lock.acquire() sleep(0.1) with self.assertRaises(redis.exceptions.LockError): lock.extend(3) class DecodeMixin: decode_responses = True def _round_str(self, x): self.assertIsInstance(x, str) return round(float(x)) @classmethod def _decode(cls, value): if isinstance(value, list): return [cls._decode(item) for item in value] elif isinstance(value, tuple): return tuple([cls._decode(item) for item in value]) elif isinstance(value, set): return {cls._decode(item) for item in value} elif isinstance(value, dict): return {cls._decode(k): cls._decode(v) for k, v in value.items()} elif isinstance(value, bytes): return value.decode('utf-8') else: return value def assertEqual(self, a, b, msg=None): super().assertEqual(a, self._decode(b), msg) def assertIn(self, member, container, msg=None): super().assertIn(self._decode(member), container) def assertItemsEqual(self, a, b): super().assertItemsEqual(a, self._decode(b)) class TestFakeStrictRedisDecodeResponses(DecodeMixin, TestFakeStrictRedis): def create_redis(self, db=0): return fakeredis.FakeStrictRedis(db=db, decode_responses=True, server=self.server) class TestFakeRedisDecodeResponses(DecodeMixin, TestFakeRedis): def create_redis(self, db=0): return fakeredis.FakeRedis(db=db, decode_responses=True, server=self.server) @redis_must_be_running class TestRealRedis(TestFakeRedis): def create_redis(self, db=0): return redis.Redis('localhost', port=6379, db=db) @redis_must_be_running class TestRealStrictRedis(TestFakeStrictRedis): def create_redis(self, db=0): return redis.StrictRedis('localhost', port=6379, db=db) @redis_must_be_running class TestRealRedisDecodeResponses(TestFakeRedisDecodeResponses): def create_redis(self, db=0): return redis.Redis('localhost', port=6379, db=db, decode_responses=True) @redis_must_be_running class TestRealStrictRedisDecodeResponses(TestFakeStrictRedisDecodeResponses): def create_redis(self, db=0): return redis.StrictRedis('localhost', port=6379, db=db, decode_responses=True) class TestInitArgs(unittest.TestCase): def test_singleton(self): shared_server = fakeredis.FakeServer() r1 = fakeredis.FakeStrictRedis() r2 = fakeredis.FakeStrictRedis() r3 = fakeredis.FakeStrictRedis(server=shared_server) r4 = fakeredis.FakeStrictRedis(server=shared_server) r1.set('foo', 'bar') r3.set('bar', 'baz') self.assertIn('foo', r1) self.assertNotIn('foo', r2) self.assertNotIn('foo', r3) self.assertIn('bar', r3) self.assertIn('bar', r4) self.assertNotIn('bar', r1) def test_from_url(self): db = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/0') db.set('foo', 'bar') self.assertEqual(db.get('foo'), b'bar') def test_from_url_with_db_arg(self): db = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/0') db1 = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/1') db2 = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/', db=2) db.set('foo', 'foo0') db1.set('foo', 'foo1') db2.set('foo', 'foo2') self.assertEqual(db.get('foo'), b'foo0') self.assertEqual(db1.get('foo'), b'foo1') self.assertEqual(db2.get('foo'), b'foo2') def test_from_url_db_value_error(self): # In ValueError, should default to 0 db = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/a') self.assertEqual(db.connection_pool.connection_kwargs['db'], 0) def test_can_pass_through_extra_args(self): db = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/0', decode_responses=True) db.set('foo', 'bar') self.assertEqual(db.get('foo'), 'bar') def test_can_allow_extra_args(self): db = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/0', socket_connect_timeout=11, socket_timeout=12, socket_keepalive=True, socket_keepalive_options={60: 30}, socket_type=1, retry_on_timeout=True, ) fake_conn = db.connection_pool.make_connection() self.assertEqual(fake_conn.socket_connect_timeout, 11) self.assertEqual(fake_conn.socket_timeout, 12) self.assertEqual(fake_conn.socket_keepalive, True) self.assertEqual(fake_conn.socket_keepalive_options, {60: 30}) self.assertEqual(fake_conn.socket_type, 1) self.assertEqual(fake_conn.retry_on_timeout, True) # Make fallback logic match redis-py db = fakeredis.FakeStrictRedis.from_url( 'redis://localhost:6379/0', socket_connect_timeout=None, socket_timeout=30 ) fake_conn = db.connection_pool.make_connection() self.assertEqual( fake_conn.socket_connect_timeout, fake_conn.socket_timeout ) self.assertEqual(fake_conn.socket_keepalive_options, {}) def test_repr(self): # repr is human-readable, so we only test that it doesn't crash, # and that it contains the db number. db = fakeredis.FakeStrictRedis.from_url('redis://localhost:6379/11') rep = repr(db) self.assertIn('db=11', rep) class TestFakeStrictRedisConnectionErrors(unittest.TestCase): # Wrap some redis commands to abstract differences between redis-py 2 and 3. def zadd(self, key, d): if REDIS3: return self.redis.zadd(key, d) else: return self.redis.zadd(key, **d) def create_redis(self): return fakeredis.FakeStrictRedis(db=0, connected=False) def setUp(self): self.redis = self.create_redis() def tearDown(self): del self.redis def test_flushdb(self): with self.assertRaises(redis.ConnectionError): self.redis.flushdb() def test_flushall(self): with self.assertRaises(redis.ConnectionError): self.redis.flushall() def test_append(self): with self.assertRaises(redis.ConnectionError): self.redis.append('key', 'value') def test_bitcount(self): with self.assertRaises(redis.ConnectionError): self.redis.bitcount('key', 0, 20) def test_decr(self): with self.assertRaises(redis.ConnectionError): self.redis.decr('key', 2) def test_exists(self): with self.assertRaises(redis.ConnectionError): self.redis.exists('key') def test_expire(self): with self.assertRaises(redis.ConnectionError): self.redis.expire('key', 20) def test_pexpire(self): with self.assertRaises(redis.ConnectionError): self.redis.pexpire('key', 20) def test_echo(self): with self.assertRaises(redis.ConnectionError): self.redis.echo('value') def test_get(self): with self.assertRaises(redis.ConnectionError): self.redis.get('key') def test_getbit(self): with self.assertRaises(redis.ConnectionError): self.redis.getbit('key', 2) def test_getset(self): with self.assertRaises(redis.ConnectionError): self.redis.getset('key', 'value') def test_incr(self): with self.assertRaises(redis.ConnectionError): self.redis.incr('key') def test_incrby(self): with self.assertRaises(redis.ConnectionError): self.redis.incrby('key') def test_ncrbyfloat(self): with self.assertRaises(redis.ConnectionError): self.redis.incrbyfloat('key') def test_keys(self): with self.assertRaises(redis.ConnectionError): self.redis.keys() def test_mget(self): with self.assertRaises(redis.ConnectionError): self.redis.mget(['key1', 'key2']) def test_mset(self): with self.assertRaises(redis.ConnectionError): self.redis.mset({'key': 'value'}) def test_msetnx(self): with self.assertRaises(redis.ConnectionError): self.redis.msetnx({'key': 'value'}) def test_persist(self): with self.assertRaises(redis.ConnectionError): self.redis.persist('key') def test_rename(self): server = self.redis.connection_pool.connection_kwargs['server'] server.connected = True self.redis.set('key1', 'value') server.connected = False with self.assertRaises(redis.ConnectionError): self.redis.rename('key1', 'key2') server.connected = True self.assertTrue(self.redis.exists('key1')) def test_eval(self): with self.assertRaises(redis.ConnectionError): self.redis.eval('', 0) def test_lpush(self): with self.assertRaises(redis.ConnectionError): self.redis.lpush('name', 1, 2) def test_lrange(self): with self.assertRaises(redis.ConnectionError): self.redis.lrange('name', 1, 5) def test_llen(self): with self.assertRaises(redis.ConnectionError): self.redis.llen('name') def test_lrem(self): with self.assertRaises(redis.ConnectionError): self.redis.lrem('name', 2, 2) def test_rpush(self): with self.assertRaises(redis.ConnectionError): self.redis.rpush('name', 1) def test_lpop(self): with self.assertRaises(redis.ConnectionError): self.redis.lpop('name') def test_lset(self): with self.assertRaises(redis.ConnectionError): self.redis.lset('name', 1, 4) def test_rpushx(self): with self.assertRaises(redis.ConnectionError): self.redis.rpushx('name', 1) def test_ltrim(self): with self.assertRaises(redis.ConnectionError): self.redis.ltrim('name', 1, 4) def test_lindex(self): with self.assertRaises(redis.ConnectionError): self.redis.lindex('name', 1) def test_lpushx(self): with self.assertRaises(redis.ConnectionError): self.redis.lpushx('name', 1) def test_rpop(self): with self.assertRaises(redis.ConnectionError): self.redis.rpop('name') def test_linsert(self): with self.assertRaises(redis.ConnectionError): self.redis.linsert('name', 'where', 'refvalue', 'value') def test_rpoplpush(self): with self.assertRaises(redis.ConnectionError): self.redis.rpoplpush('src', 'dst') def test_blpop(self): with self.assertRaises(redis.ConnectionError): self.redis.blpop('keys') def test_brpop(self): with self.assertRaises(redis.ConnectionError): self.redis.brpop('keys') def test_brpoplpush(self): with self.assertRaises(redis.ConnectionError): self.redis.brpoplpush('src', 'dst') def test_hdel(self): with self.assertRaises(redis.ConnectionError): self.redis.hdel('name') def test_hexists(self): with self.assertRaises(redis.ConnectionError): self.redis.hexists('name', 'key') def test_hget(self): with self.assertRaises(redis.ConnectionError): self.redis.hget('name', 'key') def test_hgetall(self): with self.assertRaises(redis.ConnectionError): self.redis.hgetall('name') def test_hincrby(self): with self.assertRaises(redis.ConnectionError): self.redis.hincrby('name', 'key') def test_hincrbyfloat(self): with self.assertRaises(redis.ConnectionError): self.redis.hincrbyfloat('name', 'key') def test_hkeys(self): with self.assertRaises(redis.ConnectionError): self.redis.hkeys('name') def test_hlen(self): with self.assertRaises(redis.ConnectionError): self.redis.hlen('name') def test_hset(self): with self.assertRaises(redis.ConnectionError): self.redis.hset('name', 'key', 1) def test_hsetnx(self): with self.assertRaises(redis.ConnectionError): self.redis.hsetnx('name', 'key', 2) def test_hmset(self): with self.assertRaises(redis.ConnectionError): self.redis.hmset('name', {'key': 1}) def test_hmget(self): with self.assertRaises(redis.ConnectionError): self.redis.hmget('name', ['a', 'b']) def test_hvals(self): with self.assertRaises(redis.ConnectionError): self.redis.hvals('name') def test_sadd(self): with self.assertRaises(redis.ConnectionError): self.redis.sadd('name', 1, 2) def test_scard(self): with self.assertRaises(redis.ConnectionError): self.redis.scard('name') def test_sdiff(self): with self.assertRaises(redis.ConnectionError): self.redis.sdiff(['a', 'b']) def test_sdiffstore(self): with self.assertRaises(redis.ConnectionError): self.redis.sdiffstore('dest', ['a', 'b']) def test_sinter(self): with self.assertRaises(redis.ConnectionError): self.redis.sinter(['a', 'b']) def test_sinterstore(self): with self.assertRaises(redis.ConnectionError): self.redis.sinterstore('dest', ['a', 'b']) def test_sismember(self): with self.assertRaises(redis.ConnectionError): self.redis.sismember('name', 20) def test_smembers(self): with self.assertRaises(redis.ConnectionError): self.redis.smembers('name') def test_smove(self): with self.assertRaises(redis.ConnectionError): self.redis.smove('src', 'dest', 20) def test_spop(self): with self.assertRaises(redis.ConnectionError): self.redis.spop('name') def test_srandmember(self): with self.assertRaises(redis.ConnectionError): self.redis.srandmember('name') def test_srem(self): with self.assertRaises(redis.ConnectionError): self.redis.srem('name') def test_sunion(self): with self.assertRaises(redis.ConnectionError): self.redis.sunion(['a', 'b']) def test_sunionstore(self): with self.assertRaises(redis.ConnectionError): self.redis.sunionstore('dest', ['a', 'b']) def test_zadd(self): with self.assertRaises(redis.ConnectionError): self.zadd('name', {'key': 'value'}) def test_zcard(self): with self.assertRaises(redis.ConnectionError): self.redis.zcard('name') def test_zcount(self): with self.assertRaises(redis.ConnectionError): self.redis.zcount('name', 1, 5) def test_zincrby(self): with self.assertRaises(redis.ConnectionError): self.redis.zincrby('name', 1, 1) def test_zinterstore(self): with self.assertRaises(redis.ConnectionError): self.redis.zinterstore('dest', ['a', 'b']) def test_zrange(self): with self.assertRaises(redis.ConnectionError): self.redis.zrange('name', 1, 5) def test_zrangebyscore(self): with self.assertRaises(redis.ConnectionError): self.redis.zrangebyscore('name', 1, 5) def test_rangebylex(self): with self.assertRaises(redis.ConnectionError): self.redis.zrangebylex('name', 1, 4) def test_zrem(self): with self.assertRaises(redis.ConnectionError): self.redis.zrem('name', 'value') def test_zremrangebyrank(self): with self.assertRaises(redis.ConnectionError): self.redis.zremrangebyrank('name', 1, 5) def test_zremrangebyscore(self): with self.assertRaises(redis.ConnectionError): self.redis.zremrangebyscore('name', 1, 5) def test_zremrangebylex(self): with self.assertRaises(redis.ConnectionError): self.redis.zremrangebylex('name', 1, 5) def test_zlexcount(self): with self.assertRaises(redis.ConnectionError): self.redis.zlexcount('name', 1, 5) def test_zrevrange(self): with self.assertRaises(redis.ConnectionError): self.redis.zrevrange('name', 1, 5, 1) def test_zrevrangebyscore(self): with self.assertRaises(redis.ConnectionError): self.redis.zrevrangebyscore('name', 5, 1) def test_zrevrangebylex(self): with self.assertRaises(redis.ConnectionError): self.redis.zrevrangebylex('name', 5, 1) def test_zrevran(self): with self.assertRaises(redis.ConnectionError): self.redis.zrevrank('name', 2) def test_zscore(self): with self.assertRaises(redis.ConnectionError): self.redis.zscore('name', 2) def test_zunionstor(self): with self.assertRaises(redis.ConnectionError): self.redis.zunionstore('dest', ['1', '2']) def test_pipeline(self): with self.assertRaises(redis.ConnectionError): self.redis.pipeline().watch('key') def test_transaction(self): with self.assertRaises(redis.ConnectionError): def func(a): return a * a self.redis.transaction(func, 3) def test_lock(self): with self.assertRaises(redis.ConnectionError): with self.redis.lock('name'): pass def test_pubsub(self): with self.assertRaises(redis.ConnectionError): self.redis.pubsub().subscribe('channel') def test_pfadd(self): with self.assertRaises(redis.ConnectionError): self.redis.pfadd('name', 1) def test_pfmerge(self): with self.assertRaises(redis.ConnectionError): self.redis.pfmerge('dest', 'a', 'b') def test_scan(self): with self.assertRaises(redis.ConnectionError): list(self.redis.scan()) def test_sscan(self): with self.assertRaises(redis.ConnectionError): self.redis.sscan('name') def test_hscan(self): with self.assertRaises(redis.ConnectionError): self.redis.hscan('name') def test_scan_iter(self): with self.assertRaises(redis.ConnectionError): list(self.redis.scan_iter()) def test_sscan_iter(self): with self.assertRaises(redis.ConnectionError): list(self.redis.sscan_iter('name')) def test_hscan_iter(self): with self.assertRaises(redis.ConnectionError): list(self.redis.hscan_iter('name')) class TestPubSubConnected(unittest.TestCase): def setUp(self): self.server = fakeredis.FakeServer() self.server.connected = False self.redis = fakeredis.FakeStrictRedis(server=self.server) self.pubsub = self.redis.pubsub() def test_basic_subscribe(self): with self.assertRaises(redis.ConnectionError): self.pubsub.subscribe('logs') def test_subscription_conn_lost(self): self.server.connected = True self.pubsub.subscribe('logs') self.server.connected = False # The initial message is already in the pipe msg = self.pubsub.get_message() check = { 'type': 'subscribe', 'pattern': None, 'channel': b'logs', 'data': 1 } self.assertEqual(msg, check, 'Message was not published to channel') with self.assertRaises(redis.ConnectionError): self.pubsub.get_message() if __name__ == '__main__': unittest.main() fakeredis-1.2.1/test_fakeredis_hypothesis.py000066400000000000000000000533621362144536200213140ustar00rootroot00000000000000import operator import functools import hypothesis import hypothesis.stateful from hypothesis.stateful import rule, initialize, precondition import hypothesis.strategies as st import pytest import redis import fakeredis self_strategy = st.runner() @st.composite def sample_attr(draw, name): """Strategy for sampling a specific attribute from a state machine""" machine = draw(self_strategy) values = getattr(machine, name) position = draw(st.integers(min_value=0, max_value=len(values) - 1)) return values[position] keys = sample_attr('keys') fields = sample_attr('fields') values = sample_attr('values') scores = sample_attr('scores') int_as_bytes = st.builds(lambda x: str(x).encode(), st.integers()) float_as_bytes = st.builds(lambda x: repr(x).encode(), st.floats(width=32)) counts = st.integers(min_value=-3, max_value=3) | st.integers() limits = st.just(()) | st.tuples(st.just('limit'), counts, counts) # Redis has an integer overflow bug in swapdb, so we confine the numbers to # a limited range (https://github.com/antirez/redis/issues/5737). dbnums = st.integers(min_value=0, max_value=3) | st.integers(min_value=-1000, max_value=1000) # The filter is to work around https://github.com/antirez/redis/issues/5632 patterns = (st.text(alphabet=st.sampled_from('[]^$*.?-azAZ\\\r\n\t')) | st.binary().filter(lambda x: b'\0' not in x)) score_tests = scores | st.builds(lambda x: b'(' + repr(x).encode(), scores) string_tests = ( st.sampled_from([b'+', b'-']) | st.builds(operator.add, st.sampled_from([b'(', b'[']), fields)) # Redis has integer overflow bugs in time computations, which is why we set a maximum. expires_seconds = st.integers(min_value=100000, max_value=10000000000) expires_ms = st.integers(min_value=100000000, max_value=10000000000000) class WrappedException: """Wraps an exception for the purposes of comparison.""" def __init__(self, exc): self.wrapped = exc def __str__(self): return str(self.wrapped) def __repr__(self): return 'WrappedException({!r})'.format(self.wrapped) def __eq__(self, other): if not isinstance(other, WrappedException): return NotImplemented if type(self.wrapped) != type(other.wrapped): # noqa: E721 return False # TODO: re-enable after more carefully handling order of error checks # return self.wrapped.args == other.wrapped.args return True def __ne__(self, other): if not isinstance(other, WrappedException): return NotImplemented return not self == other def wrap_exceptions(obj): if isinstance(obj, list): return [wrap_exceptions(item) for item in obj] elif isinstance(obj, Exception): return WrappedException(obj) else: return obj def sort_list(lst): if isinstance(lst, list): return sorted(lst) else: return lst def flatten(args): if isinstance(args, (list, tuple)): for arg in args: yield from flatten(arg) elif args is not None: yield args def default_normalize(x): return x class Command: def __init__(self, *args): self.args = tuple(flatten(args)) def __repr__(self): parts = [repr(arg) for arg in self.args] return 'Command({})'.format(', '.join(parts)) @staticmethod def encode(arg): encoder = redis.connection.Encoder('utf-8', 'replace', False) return encoder.encode(arg) @property def normalize(self): command = self.encode(self.args[0]).lower() if self.args else None # Functions that return a list in arbitrary order unordered = { b'keys', b'sort', b'hgetall', b'hkeys', b'hvals', b'sdiff', b'sinter', b'sunion', b'smembers' } if command in unordered: return sort_list else: return lambda x: x @property def testable(self): """Whether this command is suitable for a test. The fuzzer can create commands with behaviour that is non-deterministic, not supported, or which hits redis bugs. """ N = len(self.args) if N == 0: return False command = self.encode(self.args[0]).lower() if not command.split(): return False if command == b'keys' and N == 2 and self.args[1] != b'*': return False # redis will ignore a NUL character in some commands but not others # e.g. it recognises EXEC\0 but not MULTI\00. Rather than try to # reproduce this quirky behaviour, just skip these tests. if b'\0' in command: return False # 'incr' flag to zadd not implemented yet if command == b'zadd' and 'incr' in self.args: return False return True def commands(*args, **kwargs): return st.builds(functools.partial(Command, **kwargs), *args) # TODO: all expiry-related commands common_commands = ( commands(st.sampled_from(['del', 'persist', 'type', 'unlink']), keys) | commands(st.just('exists'), st.lists(keys)) | commands(st.just('keys'), st.just('*')) # Disabled for now due to redis giving wrong answers # (https://github.com/antirez/redis/issues/5632) # | st.tuples(st.just('keys'), patterns) | commands(st.just('move'), keys, dbnums) | commands(st.sampled_from(['rename', 'renamenx']), keys, keys) # TODO: find a better solution to sort instability than throwing # away the sort entirely with normalize. This also prevents us # using LIMIT. | commands(st.just('sort'), keys, st.none() | st.just('asc'), st.none() | st.just('desc'), st.none() | st.just('alpha')) ) # TODO: tests for select connection_commands = ( commands(st.just('echo'), values) | commands(st.just('ping'), st.lists(values, max_size=2)) | commands(st.just('swapdb'), dbnums, dbnums) ) string_create_commands = commands(st.just('set'), keys, values) string_commands = ( commands(st.just('append'), keys, values) | commands(st.just('bitcount'), keys) | commands(st.just('bitcount'), keys, values, values) | commands(st.sampled_from(['incr', 'decr']), keys) | commands(st.sampled_from(['incrby', 'decrby']), keys, values) # Disabled for now because Python can't exactly model the long doubles. # TODO: make a more targeted test that checks the basics. # TODO: check how it gets stringified, without relying on hypothesis # to get generate a get call before it gets overwritten. # | commands(st.just('incrbyfloat'), keys, st.floats(width=32)) | commands(st.just('get'), keys) | commands(st.just('getbit'), keys, counts) | commands(st.just('setbit'), keys, counts, st.integers(min_value=0, max_value=1) | st.integers()) | commands(st.sampled_from(['substr', 'getrange']), keys, counts, counts) | commands(st.just('getset'), keys, values) | commands(st.just('mget'), st.lists(keys)) | commands(st.sampled_from(['mset', 'msetnx']), st.lists(st.tuples(keys, values))) | commands(st.just('set'), keys, values, st.none() | st.just('nx'), st.none() | st.just('xx')) | commands(st.just('setex'), keys, expires_seconds, values) | commands(st.just('psetex'), keys, expires_ms, values) | commands(st.just('setnx'), keys, values) | commands(st.just('setrange'), keys, counts, values) | commands(st.just('strlen'), keys) ) # TODO: add a test for hincrbyfloat. See incrbyfloat for why this is # problematic. hash_create_commands = ( commands(st.just('hmset'), keys, st.lists(st.tuples(fields, values), min_size=1)) ) hash_commands = ( commands(st.just('hmset'), keys, st.lists(st.tuples(fields, values))) | commands(st.just('hdel'), keys, st.lists(fields)) | commands(st.just('hexists'), keys, fields) | commands(st.just('hget'), keys, fields) | commands(st.sampled_from(['hgetall', 'hkeys', 'hvals']), keys) | commands(st.just('hincrby'), keys, fields, st.integers()) | commands(st.just('hlen'), keys) | commands(st.just('hmget'), keys, st.lists(fields)) | commands(st.sampled_from(['hset', 'hmset']), keys, st.lists(st.tuples(fields, values))) | commands(st.just('hsetnx'), keys, fields, values) | commands(st.just('hstrlen'), keys, fields) ) # TODO: blocking commands list_create_commands = commands(st.just('rpush'), keys, st.lists(values, min_size=1)) list_commands = ( commands(st.just('lindex'), keys, counts) | commands(st.just('linsert'), keys, st.sampled_from(['before', 'after', 'BEFORE', 'AFTER']) | st.binary(), values, values) | commands(st.just('llen'), keys) | commands(st.sampled_from(['lpop', 'rpop']), keys) | commands(st.sampled_from(['lpush', 'lpushx', 'rpush', 'rpushx']), keys, st.lists(values)) | commands(st.just('lrange'), keys, counts, counts) | commands(st.just('lrem'), keys, counts, values) | commands(st.just('lset'), keys, counts, values) | commands(st.just('ltrim'), keys, counts, counts) | commands(st.just('rpoplpush'), keys, keys) ) # TODO: # - find a way to test srandmember, spop which are random # - sscan set_create_commands = ( commands(st.just('sadd'), keys, st.lists(fields, min_size=1)) ) set_commands = ( commands(st.just('sadd'), keys, st.lists(fields,)) | commands(st.just('scard'), keys) | commands(st.sampled_from(['sdiff', 'sinter', 'sunion']), st.lists(keys)) | commands(st.sampled_from(['sdiffstore', 'sinterstore', 'sunionstore']), keys, st.lists(keys)) | commands(st.just('sismember'), keys, fields) | commands(st.just('smembers'), keys) | commands(st.just('smove'), keys, keys, fields) | commands(st.just('srem'), keys, st.lists(fields)) ) def build_zstore(command, dest, sources, weights, aggregate): args = [command, dest, len(sources)] args += [source[0] for source in sources] if weights: args.append('weights') args += [source[1] for source in sources] if aggregate: args += ['aggregate', aggregate] return Command(args) # TODO: zscan, zpopmin/zpopmax, bzpopmin/bzpopmax, probably more zset_create_commands = ( commands(st.just('zadd'), keys, st.lists(st.tuples(scores, fields), min_size=1)) ) zset_commands = ( # TODO: test incr commands(st.just('zadd'), keys, st.none() | st.just('nx'), st.none() | st.just('xx'), st.none() | st.just('ch'), st.lists(st.tuples(scores, fields))) | commands(st.just('zcard'), keys) | commands(st.just('zcount'), keys, score_tests, score_tests) | commands(st.just('zincrby'), keys, scores, fields) | commands(st.sampled_from(['zrange', 'zrevrange']), keys, counts, counts, st.none() | st.just('withscores')) | commands(st.sampled_from(['zrangebyscore', 'zrevrangebyscore']), keys, score_tests, score_tests, limits, st.none() | st.just('withscores')) | commands(st.sampled_from(['zrank', 'zrevrank']), keys, fields) | commands(st.just('zrem'), keys, st.lists(fields)) | commands(st.just('zremrangebyrank'), keys, counts, counts) | commands(st.just('zremrangebyscore'), keys, score_tests, score_tests) | commands(st.just('zscore'), keys, fields) | st.builds(build_zstore, command=st.sampled_from(['zunionstore', 'zinterstore']), dest=keys, sources=st.lists(st.tuples(keys, float_as_bytes)), weights=st.booleans(), aggregate=st.sampled_from([None, 'sum', 'min', 'max'])) ) zset_no_score_create_commands = ( commands(st.just('zadd'), keys, st.lists(st.tuples(st.just(0), fields), min_size=1)) ) zset_no_score_commands = ( # TODO: test incr commands(st.just('zadd'), keys, st.none() | st.just('nx'), st.none() | st.just('xx'), st.none() | st.just('ch'), st.lists(st.tuples(st.just(0), fields))) | commands(st.just('zlexcount'), keys, string_tests, string_tests) | commands(st.sampled_from(['zrangebylex', 'zrevrangebylex']), keys, string_tests, string_tests, limits) | commands(st.just('zremrangebylex'), keys, string_tests, string_tests) ) transaction_commands = ( commands(st.sampled_from(['multi', 'discard', 'exec', 'unwatch'])) | commands(st.just('watch'), keys) ) server_commands = ( # TODO: real redis raises an error if there is a save already in progress. # Find a better way to test this. # commands(st.just('bgsave')) commands(st.just('dbsize')) | commands(st.sampled_from(['flushdb', 'flushall']), st.sampled_from([[], 'async'])) # TODO: result is non-deterministic # | commands(st.just('lastsave')) | commands(st.just('save')) ) bad_commands = ( # redis-py splits the command on spaces, and hangs if that ends up # being an empty list commands(st.text().filter(lambda x: bool(x.split())), st.lists(st.binary() | st.text())) ) attrs = st.fixed_dictionaries({ 'keys': st.lists(st.binary(), min_size=2, max_size=5, unique=True), 'fields': st.lists(st.binary(), min_size=2, max_size=5, unique=True), 'values': st.lists(st.binary() | int_as_bytes | float_as_bytes, min_size=2, max_size=5, unique=True), 'scores': st.lists(st.floats(width=32), min_size=2, max_size=5, unique=True) }) @hypothesis.settings(max_examples=1000) class CommonMachine(hypothesis.stateful.RuleBasedStateMachine): create_command_strategy = st.nothing() def __init__(self): super().__init__() self.fake = fakeredis.FakeStrictRedis() try: self.real = redis.StrictRedis('localhost', port=6379) self.real.ping() except redis.ConnectionError: pytest.skip('redis is not running') # Disable the response parsing so that we can check the raw values returned self.fake.response_callbacks.clear() self.real.response_callbacks.clear() self.transaction_normalize = [] self.keys = [] self.fields = [] self.values = [] self.scores = [] self.initialized_data = False try: self.real.execute_command('discard') except redis.ResponseError: pass self.real.flushall() def teardown(self): self.real.connection_pool.disconnect() self.fake.connection_pool.disconnect() super().teardown() def _evaluate(self, client, command): try: result = client.execute_command(*command.args) if result != 'QUEUED': result = command.normalize(result) exc = None except Exception as e: result = exc = e return wrap_exceptions(result), exc def _compare(self, command): fake_result, fake_exc = self._evaluate(self.fake, command) real_result, real_exc = self._evaluate(self.real, command) if fake_exc is not None and real_exc is None: raise fake_exc elif real_exc is not None and fake_exc is None: assert real_exc == fake_exc, "Expected exception {} not raised".format(real_exc) elif (real_exc is None and isinstance(real_result, list) and command.args and command.args[0].lower() == 'exec'): assert fake_result is not None # Transactions need to use the normalize functions of the # component commands. assert len(self.transaction_normalize) == len(real_result) assert len(self.transaction_normalize) == len(fake_result) for n, r, f in zip(self.transaction_normalize, real_result, fake_result): assert n(f) == n(r) self.transaction_normalize = [] else: assert fake_result == real_result if real_result == b'QUEUED': # Since redis removes the distinction between simple strings and # bulk strings, this might not actually indicate that we're in a # transaction. But it is extremely unlikely that hypothesis will # find such examples. self.transaction_normalize.append(command.normalize) if (len(command.args) == 1 and Command.encode(command.args[0]).lower() in (b'discard', b'exec')): self.transaction_normalize = [] @initialize(attrs=attrs) def init_attrs(self, attrs): for key, value in attrs.items(): setattr(self, key, value) # hypothesis doesn't allow ordering of @initialize, so we have to put # preconditions on rules to ensure we call init_data exactly once and # after init_attrs. @precondition(lambda self: not self.initialized_data) @rule(commands=self_strategy.flatmap( lambda self: st.lists(self.create_command_strategy))) def init_data(self, commands): for command in commands: self._compare(command) self.initialized_data = True @precondition(lambda self: self.initialized_data) @rule(command=self_strategy.flatmap(lambda self: self.command_strategy)) def one_command(self, command): self._compare(command) class BaseTest: """Base class for test classes.""" create_command_strategy = st.nothing() @pytest.mark.slow def test(self): class Machine(CommonMachine): create_command_strategy = self.create_command_strategy command_strategy = self.command_strategy hypothesis.stateful.run_state_machine_as_test(Machine) class TestConnection(BaseTest): command_strategy = connection_commands | common_commands class TestString(BaseTest): create_command_strategy = string_create_commands command_strategy = string_commands | common_commands class TestHash(BaseTest): create_command_strategy = hash_create_commands command_strategy = hash_commands | common_commands class TestList(BaseTest): create_command_strategy = list_create_commands command_strategy = list_commands | common_commands class TestSet(BaseTest): create_command_strategy = set_create_commands command_strategy = set_commands | common_commands class TestZSet(BaseTest): create_command_strategy = zset_create_commands command_strategy = zset_commands | common_commands class TestZSetNoScores(BaseTest): create_command_strategy = zset_no_score_create_commands command_strategy = zset_no_score_commands | common_commands class TestTransaction(BaseTest): create_command_strategy = string_create_commands command_strategy = transaction_commands | string_commands | common_commands class TestServer(BaseTest): create_command_strategy = string_create_commands command_strategy = server_commands | string_commands | common_commands class TestJoint(BaseTest): create_command_strategy = ( string_create_commands | hash_create_commands | list_create_commands | set_create_commands | zset_create_commands) command_strategy = ( transaction_commands | server_commands | connection_commands | string_commands | hash_commands | list_commands | set_commands | zset_commands | common_commands | bad_commands) @st.composite def delete_arg(draw, commands): command = draw(commands) if command.args: pos = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) command.args = command.args[:pos] + command.args[pos + 1:] return command @st.composite def command_args(draw, commands): """Generate an argument from some command""" command = draw(commands) hypothesis.assume(len(command.args)) return draw(st.sampled_from(command.args)) def mutate_arg(draw, commands, mutate): command = draw(commands) if command.args: pos = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) arg = mutate(Command.encode(command.args[pos])) command.args = command.args[:pos] + (arg,) + command.args[pos + 1:] return command @st.composite def replace_arg(draw, commands, replacements): return mutate_arg(draw, commands, lambda arg: draw(replacements)) @st.composite def uppercase_arg(draw, commands): return mutate_arg(draw, commands, lambda arg: arg.upper()) @st.composite def prefix_arg(draw, commands, prefixes): return mutate_arg(draw, commands, lambda arg: draw(prefixes) + arg) @st.composite def suffix_arg(draw, commands, suffixes): return mutate_arg(draw, commands, lambda arg: arg + draw(suffixes)) @st.composite def add_arg(draw, commands, arguments): command = draw(commands) arg = draw(arguments) pos = draw(st.integers(min_value=0, max_value=len(command.args))) command.args = command.args[:pos] + (arg,) + command.args[pos:] return command @st.composite def swap_args(draw, commands): command = draw(commands) if len(command.args) >= 2: pos1 = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) pos2 = draw(st.integers(min_value=0, max_value=len(command.args) - 1)) hypothesis.assume(pos1 != pos2) args = list(command.args) arg1 = args[pos1] arg2 = args[pos2] args[pos1] = arg2 args[pos2] = arg1 command.args = tuple(args) return command def mutated_commands(commands): args = st.sampled_from([b'withscores', b'xx', b'nx', b'ex', b'px', b'weights', b'aggregate', b'', b'0', b'-1', b'nan', b'inf', b'-inf']) | command_args(commands) affixes = st.sampled_from([b'\0', b'-', b'+', b'\t', b'\n', b'0000']) | st.binary() return st.recursive( commands, lambda x: delete_arg(x) | replace_arg(x, args) | uppercase_arg(x) | prefix_arg(x, affixes) | suffix_arg(x, affixes) | add_arg(x, args) | swap_args(x)) class TestFuzz(BaseTest): command_strategy = mutated_commands(TestJoint.command_strategy) command_strategy = command_strategy.filter(lambda command: command.testable) fakeredis-1.2.1/tox.ini000066400000000000000000000002241362144536200147670ustar00rootroot00000000000000[tox] envlist = py{27,34,35,36,37,py} [testenv] usedevelop = True commands = pytest -v {posargs} extras = lua deps = hypothesis pytest