├── mockredis ├── tests │ ├── __init__.py │ ├── test_constants.py │ ├── test_pubsub.py │ ├── test_config.py │ ├── test_factories.py │ ├── fixtures.py │ ├── test_normalize.py │ ├── test_sortedset.py │ ├── test_hash.py │ ├── test_pipeline.py │ ├── test_redis.py │ ├── test_set.py │ ├── test_scan.py │ ├── test_string.py │ ├── test_script.py │ ├── test_list.py │ └── test_zset.py ├── __init__.py ├── clock.py ├── exceptions.py ├── lock.py ├── pubsub.py ├── pipeline.py ├── noseplugin.py ├── sortedset.py ├── script.py └── client.py ├── tox.ini ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── setup.py ├── README.md ├── CHANGES.md └── LICENSE /mockredis/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mockredis/__init__.py: -------------------------------------------------------------------------------- 1 | from mockredis.client import MockRedis, mock_redis_client, mock_strict_redis_client 2 | 3 | __all__ = ["MockRedis", "mock_redis_client", "mock_strict_redis_client"] 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py32, py33, py34, pypy, lint 3 | 4 | [testenv] 5 | commands = python setup.py nosetests 6 | 7 | [testenv:lint] 8 | commands=flake8 --max-line-length 99 mockredis 9 | basepython=python2.7 10 | deps= 11 | flake8 12 | flake8-print 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /mockredis/tests/test_constants.py: -------------------------------------------------------------------------------- 1 | LIST1 = "test_list_1" 2 | LIST2 = "test_list_2" 3 | 4 | SET1 = "test_set_1" 5 | SET2 = "test_set_2" 6 | 7 | VAL1 = "val1" 8 | VAL2 = "val2" 9 | VAL3 = "val3" 10 | VAL4 = "val4" 11 | 12 | LPOP_SCRIPT = "return redis.call('LPOP', KEYS[1])" 13 | 14 | bVAL1 = VAL1.encode('utf8') 15 | bVAL2 = VAL2.encode('utf8') 16 | bVAL3 = VAL3.encode('utf8') 17 | bVAL4 = VAL4.encode('utf8') 18 | 19 | bLIST1 = LIST1.encode('utf8') 20 | bLIST2 = LIST2.encode('utf8') 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.2' 5 | - '3.3' 6 | - '3.4' 7 | - pypy 8 | - pypy3 9 | install: pip install . 10 | script: python setup.py nosetests 11 | deploy: 12 | provider: pypi 13 | user: github-ll 14 | password: 15 | secure: WZNNslmr6ELiasA6IhO9QDQ/g7758WjezVLqC3Ebo3EgAsUOFeCvtqODf3U8czz4UxORrVL7xWN/lUEOCG/ImDoXa9W27h8kl3D0pP9s8FDNngZspB5c3xhZhQesiqB6n8Fyi2dLic9QYUUIgquo2w+w/r5rRHvplI9OVbIGuVM= 16 | on: 17 | tags: true 18 | repo: locationlabs/mockredis 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Mock Redis uses GitHub Flow for its branch management. 4 | We ask that contributors to this project please: 5 | 6 | 1. Folow [GitHub Flow][1]. 7 | 8 | 2. Limit the scope of changes to a single bug fix or feature per branch. 9 | 10 | 3. Treat documentation and unit tests as an essential part of any change. 11 | 12 | 4. Folow existing style, contributions should look as part of the project. 13 | 14 | Thank you! 15 | 16 | [1]: https://guides.github.com/introduction/flow/ 17 | -------------------------------------------------------------------------------- /mockredis/tests/test_pubsub.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for pubsub don't yet support verification against redis-server. 3 | """ 4 | from nose.tools import eq_ 5 | 6 | from mockredis import MockRedis 7 | 8 | 9 | class TestRedisPubSub(object): 10 | 11 | def setup(self): 12 | self.redis = MockRedis() 13 | self.redis.flushdb() 14 | 15 | def test_publish(self): 16 | channel = 'ch#1' 17 | msg = 'test message' 18 | self.redis.publish(channel, msg) 19 | eq_(self.redis.pubsub()[channel], [msg]) 20 | -------------------------------------------------------------------------------- /mockredis/clock.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple clock abstraction. 3 | """ 4 | from abc import ABCMeta, abstractmethod 5 | from datetime import datetime 6 | 7 | 8 | class Clock(object): 9 | """ 10 | A clock knows the current time. 11 | 12 | Clock can be subclassed for testing scenarios that need to control for time. 13 | """ 14 | __metaclass__ = ABCMeta 15 | 16 | @abstractmethod 17 | def now(self): 18 | pass 19 | 20 | 21 | class SystemClock(Clock): 22 | 23 | def now(self): 24 | return datetime.now() 25 | -------------------------------------------------------------------------------- /mockredis/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Emulates exceptions raised by the Redis client, if necessary. 3 | """ 4 | 5 | try: 6 | # Prefer actual exceptions to defining our own, so code that swaps 7 | # in implementations does not have to swap in different exception 8 | # classes. 9 | from redis.exceptions import RedisError, ResponseError, WatchError 10 | except ImportError: 11 | class RedisError(Exception): 12 | pass 13 | 14 | class ResponseError(RedisError): 15 | pass 16 | 17 | class WatchError(RedisError): 18 | pass 19 | -------------------------------------------------------------------------------- /mockredis/tests/test_config.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | 3 | from mockredis.tests.fixtures import setup, teardown 4 | 5 | 6 | class TestRedisConfig(object): 7 | """Redis config set/get tests""" 8 | 9 | def setup(self): 10 | setup(self) 11 | 12 | def teardown(self): 13 | teardown(self) 14 | 15 | def test_config_set(self): 16 | eq_(self.redis.config_get('config-param'), {}) 17 | self.redis.config_set('config-param', 'value') 18 | eq_(self.redis.config_get('config-param'), {'config-param': 'value'}) 19 | eq_(self.redis.config_get('config*'), {'config-param': 'value'}) 20 | 21 | -------------------------------------------------------------------------------- /mockredis/lock.py: -------------------------------------------------------------------------------- 1 | class MockRedisLock(object): 2 | """ 3 | Poorly imitate a Redis lock object from redis-py 4 | to allow testing without a real redis server. 5 | """ 6 | 7 | def __init__(self, redis, name, timeout=None, sleep=0.1): 8 | """Initialize the object.""" 9 | 10 | self.redis = redis 11 | self.name = name 12 | self.acquired_until = None 13 | self.timeout = timeout 14 | self.sleep = sleep 15 | 16 | def acquire(self, blocking=True): # pylint: disable=R0201,W0613 17 | """Emulate acquire.""" 18 | 19 | return True 20 | 21 | def release(self): # pylint: disable=R0201 22 | """Emulate release.""" 23 | 24 | return 25 | 26 | def __enter__(self): 27 | return self.acquire() 28 | 29 | def __exit__(self, exc_type, exc_value, traceback): 30 | self.release() 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | # Match releases to redis-py versions 6 | __version__ = '2.9.3' 7 | 8 | # Jenkins will replace __build__ with a unique value. 9 | __build__ = '' 10 | 11 | setup(name='mockredispy', 12 | version=__version__ + __build__, 13 | description='Mock for redis-py', 14 | url='http://www.github.com/locationlabs/mockredis', 15 | license='Apache2', 16 | packages=find_packages(exclude=['*.tests']), 17 | setup_requires=[ 18 | 'nose' 19 | ], 20 | extras_require={ 21 | 'lua': ['lunatic-python-bugfix==1.1.1'], 22 | }, 23 | tests_require=[ 24 | 'redis>=2.9.0' 25 | ], 26 | test_suite='mockredis.tests', 27 | entry_points={ 28 | 'nose.plugins.0.10': [ 29 | 'with_redis = mockredis.noseplugin:WithRedis' 30 | ] 31 | }) 32 | -------------------------------------------------------------------------------- /mockredis/tests/test_factories.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test redis client factory functions. 3 | """ 4 | from nose.tools import ok_ 5 | 6 | from mockredis import mock_redis_client, mock_strict_redis_client 7 | 8 | 9 | def test_mock_redis_client(): 10 | """ 11 | Test that we can pass kwargs to the Redis mock/patch target. 12 | """ 13 | ok_(not mock_redis_client(host="localhost", port=6379).strict) 14 | 15 | 16 | def test_mock_redis_client_from_url(): 17 | """ 18 | Test that we can pass kwargs to the Redis from_url mock/patch target. 19 | """ 20 | ok_(not mock_redis_client.from_url(host="localhost", port=6379).strict) 21 | 22 | 23 | def test_mock_strict_redis_client(): 24 | """ 25 | Test that we can pass kwargs to the StrictRedis mock/patch target. 26 | """ 27 | ok_(mock_strict_redis_client(host="localhost", port=6379).strict) 28 | 29 | 30 | def test_mock_strict_redis_client_from_url(): 31 | """ 32 | Test that we can pass kwargs to the StrictRedis from_url mock/patch target. 33 | """ 34 | ok_(mock_strict_redis_client.from_url(host="localhost", port=6379).strict) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mock for the redis-py client library 2 | 3 | Supports writing tests for code using the [redis-py][redis-py] library 4 | without requiring a [redis-server][redis] install. 5 | 6 | [![Build Status](https://travis-ci.org/locationlabs/mockredis.png)](https://travis-ci.org/locationlabs/mockredis) 7 | 8 | ## Installation 9 | 10 | Use pip: 11 | 12 | pip install mockredispy 13 | 14 | ## Usage 15 | 16 | Both `mockredis.mock_redis_client` and `mockredis.mock_strict_redis_client` can be 17 | used to patch instances of the *redis client*. 18 | 19 | For example, using the [mock][mock] library: 20 | 21 | @patch('redis.Redis', mock_redis_client) 22 | 23 | Or: 24 | 25 | @patch('redis.StrictRedis', mock_strict_redis_client) 26 | 27 | ## Testing 28 | 29 | Many unit tests exist to verify correctness of mock functionality. In addition, most 30 | unit tests support testing against an actual redis-server instance to verify the tests 31 | against ground truth. See `mockredis.tests.fixtures` for more details and disclaimers. 32 | 33 | ## Supported python versions 34 | 35 | - Python 2.7 36 | - Python 3.2 37 | - Python 3.3 38 | - Python 3.4 39 | - PyPy 40 | - PyPy3 41 | 42 | ## Attribution 43 | 44 | This code is shamelessly derived from work by [John DeRosa][john]. 45 | 46 | [redis-py]: https://github.com/andymccurdy/redis-py 47 | [redis]: http://redis.io 48 | [john]: http://seeknuance.com/2012/02/18/replacing-redis-with-a-python-mock/ 49 | [mock]: http://www.voidspace.org.uk/python/mock/ 50 | -------------------------------------------------------------------------------- /mockredis/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test fixtures for mockredis using the WithRedis plugin. 3 | """ 4 | from contextlib import contextmanager 5 | 6 | from nose.tools import assert_raises, raises 7 | 8 | from mockredis.noseplugin import WithRedis 9 | 10 | 11 | def setup(self): 12 | """ 13 | Test setup fixtures. Creates and flushes redis/strict redis instances. 14 | """ 15 | self.redis = WithRedis.Redis() 16 | self.redis_strict = WithRedis.StrictRedis() 17 | self.redis.flushdb() 18 | self.redis_strict.flushdb() 19 | 20 | 21 | def teardown(self): 22 | """ 23 | Test teardown fixtures. 24 | """ 25 | if self.redis: 26 | del self.redis 27 | if self.redis_strict: 28 | del self.redis_strict 29 | 30 | 31 | def raises_response_error(func): 32 | """ 33 | Test decorator that handles ResponseError or its mock equivalent 34 | (currently ValueError). 35 | 36 | mockredis does not currently raise redis-py's exceptions because it 37 | does not current depend on redis-py strictly. 38 | """ 39 | return raises(WithRedis.ResponseError)(func) 40 | 41 | 42 | @contextmanager 43 | def assert_raises_redis_error(): 44 | """ 45 | Test context manager that asserts that a RedisError or its mock equivalent 46 | (currently `redis.exceptions.RedisError`) were raised. 47 | 48 | mockredis does not currently raise redis-py's exceptions because it 49 | does not current depend on redis-py strictly. 50 | """ 51 | with assert_raises(WithRedis.RedisError) as capture: 52 | yield capture 53 | 54 | 55 | @contextmanager 56 | def assert_raises_watch_error(): 57 | """ 58 | Test context manager that asserts that a WatchError or its mock equivalent 59 | (currently `watch.exceptions.WatchError`) were raised. 60 | 61 | mockwatch does not currently raise watch-py's exceptions because it 62 | does not current depend on watch-py strictly. 63 | """ 64 | with assert_raises(WithRedis.WatchError) as capture: 65 | yield capture 66 | -------------------------------------------------------------------------------- /mockredis/tests/test_normalize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test redis command normalization. 3 | """ 4 | from nose.tools import eq_ 5 | 6 | from mockredis.client import MockRedis 7 | 8 | 9 | def test_normalize_command_name(): 10 | cases = [ 11 | ("DEL", "delete"), 12 | ("del", "delete"), 13 | ("ping", "ping"), 14 | ("PING", "ping"), 15 | ] 16 | 17 | def _test(command, expected): 18 | redis = MockRedis() 19 | eq_(redis._normalize_command_name(command), expected) 20 | 21 | for command, expected in cases: 22 | yield _test, command, expected 23 | 24 | 25 | def test_normalize_command_args(): 26 | 27 | cases = [ 28 | (False, "zadd", ("key", "member", 1.0), ("key", 1.0, "member")), 29 | (True, "zadd", ("key", 1.0, "member"), ("key", 1.0, "member")), 30 | 31 | (True, "zrevrangebyscore", 32 | ("key", "inf", "-inf"), 33 | ("key", "inf", "-inf")), 34 | 35 | (True, "zrevrangebyscore", 36 | ("key", "inf", "-inf", "limit", 0, 10), 37 | ("key", "inf", "-inf", 0, 10, False)), 38 | 39 | (True, "zrevrangebyscore", 40 | ("key", "inf", "-inf", "withscores"), 41 | ("key", "inf", "-inf", None, None, True)), 42 | 43 | (True, "zrevrangebyscore", 44 | ("key", "inf", "-inf", "withscores", "limit", 0, 10), 45 | ("key", "inf", "-inf", 0, 10, True)), 46 | 47 | (True, "zrevrangebyscore", 48 | ("key", "inf", "-inf", "WITHSCORES", "LIMIT", 0, 10), 49 | ("key", "inf", "-inf", 0, 10, True)), 50 | ] 51 | 52 | def _test(strict, command, args, expected): 53 | redis = MockRedis(strict=strict) 54 | eq_(tuple(redis._normalize_command_args(command, *args)), expected) 55 | 56 | for strict, command, args, expected in cases: 57 | yield _test, strict, command, args, expected 58 | 59 | 60 | def test_normalize_command_response(): 61 | 62 | cases = [ 63 | ("get", "foo", "foo"), 64 | ("zrevrangebyscore", [(1, 2), (3, 4)], [1, 2, 3, 4]), 65 | ] 66 | 67 | def _test(command, response, expected): 68 | redis = MockRedis() 69 | eq_(redis._normalize_command_response(command, response), expected) 70 | 71 | for command, response, expected in cases: 72 | yield _test, command, response, expected 73 | -------------------------------------------------------------------------------- /mockredis/pubsub.py: -------------------------------------------------------------------------------- 1 | 2 | class Pubsub(dict): 3 | 4 | def __init__(self, connection_pool, shard_hint=None, 5 | ignore_subscribe_messages=False): 6 | 7 | # there is no connection pool, but we want a reference to the parent 8 | self.redis = connection_pool 9 | self.shard_hint = shard_hint 10 | self.ignore_subscribe_messages=ignore_subscribe_messages 11 | 12 | self.channels = [] 13 | self.patterns = [] 14 | 15 | def publish(self, channel, message): 16 | """ emulate publish """ 17 | if not channel in self: 18 | self.channels.append(channel) 19 | self[channel] = [] 20 | 21 | self[channel].append(message) 22 | 23 | def reset(self): 24 | """ emulate reset """ 25 | self.clear() 26 | self.channels = [] 27 | 28 | 29 | def close(self): 30 | """ emulate close """ 31 | self.reset() 32 | 33 | def encode(self, value): 34 | """ emulate encode by calling the parent's """ 35 | return self.redis._encode(value) 36 | 37 | def on_connect(self, connection): 38 | """ do nothing while mocking """ 39 | pass 40 | 41 | @property 42 | def subscribed(self): 43 | """ emulate subscribed """ 44 | return bool(self.channels or self.patterns) 45 | 46 | 47 | def execute_command(self, *args, **kwargs): 48 | """ do nothing while mocking """ 49 | return 50 | 51 | def parse_response(self, block=True, timeout=0): 52 | """ do nothing while mocking """ 53 | return 54 | 55 | def psubscribe(self, *args, **kwargs): 56 | """ call no callbacks while mocking """ 57 | return 58 | 59 | def punsubscribe(self, *args, **kwargs): 60 | """ call no callbacks while mocking """ 61 | return 62 | 63 | def subscribe(self, *args, **kwargs): 64 | """ call no callbacks while mocking """ 65 | return 66 | 67 | def unsubscribe(self, *args, **kwargs): 68 | """ call no callbacks while mocking """ 69 | return 70 | 71 | def listen(self): 72 | """ do nothing while mocking """ 73 | return 74 | 75 | def get_message(self, ignore_subscribe_messages=False, timeout=0): 76 | """ do nothing while mocking """ 77 | return 78 | 79 | def handle_message(self, response, ignore_subscribe_messages=False): 80 | """ do nothing while mocking """ 81 | return 82 | 83 | def run_in_thread(self, sleep_time=0): 84 | """ do nothing while mocking """ 85 | return 86 | 87 | -------------------------------------------------------------------------------- /mockredis/pipeline.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from mockredis.exceptions import RedisError, WatchError 4 | 5 | 6 | class MockRedisPipeline(object): 7 | """ 8 | Simulates a redis-python pipeline object. 9 | """ 10 | 11 | def __init__(self, mock_redis, transaction=True, shard_hint=None): 12 | self.mock_redis = mock_redis 13 | self._reset() 14 | 15 | def __getattr__(self, name): 16 | """ 17 | Handle all unfound attributes by adding a deferred function call that 18 | delegates to the underlying mock redis instance. 19 | """ 20 | command = getattr(self.mock_redis, name) 21 | if not callable(command): 22 | raise AttributeError(name) 23 | 24 | def wrapper(*args, **kwargs): 25 | if self.watching and not self.explicit_transaction: 26 | # execute the command immediately 27 | return command(*args, **kwargs) 28 | else: 29 | self.commands.append(lambda: command(*args, **kwargs)) 30 | return self 31 | return wrapper 32 | 33 | def watch(self, *keys): 34 | """ 35 | Put the pipeline into immediate execution mode. 36 | Does not actually watch any keys. 37 | """ 38 | if self.explicit_transaction: 39 | raise RedisError("Cannot issue a WATCH after a MULTI") 40 | self.watching = True 41 | for key in keys: 42 | self._watched_keys[key] = deepcopy(self.mock_redis.redis.get(self.mock_redis._encode(key))) # noqa 43 | 44 | def multi(self): 45 | """ 46 | Start a transactional block of the pipeline after WATCH commands 47 | are issued. End the transactional block with `execute`. 48 | """ 49 | if self.explicit_transaction: 50 | raise RedisError("Cannot issue nested calls to MULTI") 51 | if self.commands: 52 | raise RedisError("Commands without an initial WATCH have already been issued") 53 | self.explicit_transaction = True 54 | 55 | def execute(self): 56 | """ 57 | Execute all of the saved commands and return results. 58 | """ 59 | try: 60 | for key, value in self._watched_keys.items(): 61 | if self.mock_redis.redis.get(self.mock_redis._encode(key)) != value: 62 | raise WatchError("Watched variable changed.") 63 | return [command() for command in self.commands] 64 | finally: 65 | self._reset() 66 | 67 | def _reset(self): 68 | """ 69 | Reset instance variables. 70 | """ 71 | self.commands = [] 72 | self.watching = False 73 | self._watched_keys = {} 74 | self.explicit_transaction = False 75 | 76 | def __exit__(self, *argv, **kwargs): 77 | pass 78 | 79 | def __enter__(self, *argv, **kwargs): 80 | return self 81 | -------------------------------------------------------------------------------- /mockredis/noseplugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module includes a nose plugin that allows unit tests to be run with a real 3 | redis-server instance, as long as redis-py is installed. 4 | 5 | This provides a simple way to verify that mockredis tests are accurate (at least 6 | for a particular version of redis-server and redis-py). 7 | 8 | Usage: 9 | 10 | nosetests --use-redis [--redis-host ] [--redis-database ] [args] 11 | 12 | For this plugin to work, several things need to be true: 13 | 14 | 1. Nose and setuptools need to be used to invoke tests (so the plugin will work). 15 | 16 | Note that the setuptools "entry_point" for "nose.plugins.0.10" must be activated. 17 | 18 | 2. A version of redis-py must be installed in the virtualenv under test. 19 | 20 | 3. A redis-server instance must be running locally. 21 | 22 | 4. The redis-server must have a database that can be flushed between tests. 23 | 24 | YOU WILL LOSE DATA OTHERWISE. 25 | 26 | By default, database 15 is used. 27 | 28 | 5. Tests must be written without any references to internal mockredis state. Essentially, 29 | that means testing GET and SET together instead of separately and not looking at the contents 30 | of `self.redis.redis` (because this won't exist for redis-py). 31 | """ 32 | from functools import partial 33 | import os 34 | 35 | from nose.plugins import Plugin 36 | 37 | from mockredis import MockRedis 38 | 39 | 40 | class WithRedis(Plugin): 41 | """ 42 | Nose plugin to allow selection of redis-server. 43 | """ 44 | def options(self, parser, env=os.environ): 45 | parser.add_option("--use-redis", 46 | dest="use_redis", 47 | action="store_true", 48 | default=False, 49 | help="Use a local redis instance to validate tests.") 50 | parser.add_option("--redis-host", 51 | dest="redis_host", 52 | default="localhost", 53 | help="Run tests against redis database on another host") 54 | parser.add_option("--redis-database", 55 | dest="redis_database", 56 | default=15, 57 | help="Run tests against local redis database") 58 | 59 | def configure(self, options, conf): 60 | if options.use_redis: 61 | from redis import Redis, RedisError, ResponseError, StrictRedis, WatchError 62 | 63 | WithRedis.Redis = partial(Redis, 64 | db=options.redis_database, 65 | host=options.redis_host) 66 | WithRedis.StrictRedis = partial(StrictRedis, 67 | db=options.redis_database, 68 | host=options.redis_host) 69 | WithRedis.ResponseError = ResponseError 70 | WithRedis.RedisError = RedisError 71 | WithRedis.WatchError = WatchError 72 | else: 73 | from mockredis.exceptions import RedisError, ResponseError, WatchError 74 | 75 | WithRedis.Redis = MockRedis 76 | WithRedis.StrictRedis = partial(MockRedis, strict=True) 77 | WithRedis.ResponseError = ResponseError 78 | WithRedis.RedisError = RedisError 79 | WithRedis.WatchError = WatchError 80 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Version 2.9.3 2 | - Support for `from_url` 3 | - Going to remove develop and use master following github flow model. 4 | 5 | Version 2.9.2 6 | - Fixed the versioning issue. 7 | 8 | Version 2.9.1 9 | - Support for `transaction` 10 | - Fix `do_expire` method in Python 3 11 | 12 | Version 2.9.0.12 13 | - Support: `dbsize` 14 | 15 | Version 2.9.0.11 16 | - Support: `scan_iter`, `sscan_iter`, `zscan_iter`, `hscan_iter` 17 | 18 | Version 2.9.0.10 19 | - Return & store byte strings everywhere (unicode turns into utf-8 by default) 20 | - Fix *SCAN returning non-long values. 21 | - Fix *TTL returning -1/-2/None - this varies depending on whether StrictRedis is in use. 22 | 23 | Version 2.9.0.9 24 | 25 | - Support: RENAME and RENAMENX 26 | - SADD will raise an exception if an empty list is passed to it 27 | 28 | Version 2.9.0.8 29 | 30 | - Add inclusive syntax (parenthesis) support for zero sets ZRANGEBYSCORE, ZREVRANGEBYSCORE & ZREMRANGEBYSCORE 31 | - Expire can accept a timedelta value 32 | 33 | Version 2.9.0.1 34 | 35 | - Rename `redis.py` module as `client.py` to avoid naming conflicts from the nose plugin. 36 | - Support contextmanager uses of `MockRedisLock` 37 | - Support `string` operation: MSET 38 | 39 | Version 2.9.0.0 40 | 41 | - Support redis-py 2.9.0 42 | - Support: SCAN, SSCAN, HSCAN, and ZSCAN 43 | 44 | Version 2.8.0.3 45 | 46 | - Support verifying unit tests against actual redis-server and redis-py. 47 | - Improve exception representation/mapping. 48 | - Update TTL to return -2 for unknown keys. 49 | - Fix `zset` `score_range_func` behavior to expect string input 50 | - Raise `WatchError` in `MockRedisPipeline.execute()` 51 | - Added `list` operations: SORT 52 | 53 | Version 2.8.0.2 54 | 55 | - Added `string` operations: MGET, MSETNX, and GETSET 56 | - Added "*" support to KEYS 57 | - Added container functions: __getitem__, __setitem__, __delitem__, __member__ 58 | - Added `pubsub` operations: PUBLISH 59 | 60 | Version 2.8.0.1 61 | 62 | - Fixed for RPOPLPUSH 63 | 64 | Version 2.8.0.0 65 | 66 | - Update LREM argument order to match redispy 67 | 68 | Version 2.7.5.2 69 | 70 | - Added `list` operations: LSET, LTRIM 71 | - Added `key` operations: INCRBY, DECRBY 72 | - Added `transaction` operations: WATCH, MULTI, UNWATCH 73 | - Added expiration operations: EXPIREAT, PEXPIRE, PTTL, PSETX 74 | - Fixed return values for some `set` operations 75 | 76 | Version 2.7.5.1 77 | 78 | - Changed DEL to support a list of keys as arguments and return the number of 79 | keys that were deleted. 80 | - Improved pipeline support 81 | 82 | Version 2.7.5.0 83 | 84 | - Added `script` operations: EVAL, EVALSHA, SCRIPT_EXISTS, SCRIPT_FLUSH, 85 | SCRIPT_LOAD, REGISTER_SCRIPT 86 | - Added `list` operations: RPOPLPUSH 87 | - Added `string` operations: SETEX, SETNX 88 | - Changed `string` operation SET to support EX, PX, NX and XX options 89 | (available in redis-py since 2.7.4). 90 | 91 | Version 2.7.2.5 92 | 93 | - Added `hash` operations: HMGET, HSETNX, HINCRBYFLOAT, HKEYS, HVALS 94 | 95 | Version 2.7.2.4 96 | 97 | - Added `list` operations: LREM 98 | 99 | Version 2.7.2.3 100 | 101 | - Changed distribution name to "mockredispy" 102 | - Added `set` operations: SADD (multivalue), SCARD, SDIFF, SDIFFSTORE, 103 | SINTER, SINTERSTORE, SISMEMBER, SMEMBERS (minor improvement), SMOVE, 104 | SPOP, SRANDMEMBER (improvement), SREM (multivalue), SUNION, SUNIONSTORE 105 | 106 | Version 2.7.2.2 107 | 108 | - Added `list` operations: LLEN, LPUSH, RPOP 109 | - Ensure that saved values are strings. 110 | 111 | Version 2.7.2.1 112 | 113 | - Added `zset` operations: ZADD, ZCARD, ZCOUNT, ZINCRBY, ZINTERSTORE, ZRANGE, 114 | ZRANGEBYSCORE, ZRANK, ZREM, ZREMRANGEBYRANK, ZREMRANGEBYSCORE, ZREVRANGE, 115 | ZREVRANGEBYSCORE, ZREVRANK, ZSCORE, ZUNIONSTORE 116 | -------------------------------------------------------------------------------- /mockredis/tests/test_sortedset.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_raises, eq_, ok_ 2 | 3 | from mockredis.sortedset import SortedSet 4 | 5 | 6 | class TestSortedSet(object): 7 | """ 8 | Tests the sorted set data structure, not the redis commands. 9 | """ 10 | 11 | def setup(self): 12 | self.zset = SortedSet() 13 | 14 | def test_initially_empty(self): 15 | """ 16 | Sorted set is created empty. 17 | """ 18 | eq_(0, len(self.zset)) 19 | 20 | def test_insert(self): 21 | """ 22 | Insertion maintains order and uniqueness. 23 | """ 24 | # insert two values 25 | ok_(self.zset.insert("one", 1.0)) 26 | ok_(self.zset.insert("two", 2.0)) 27 | 28 | # validate insertion 29 | eq_(2, len(self.zset)) 30 | ok_("one" in self.zset) 31 | ok_("two" in self.zset) 32 | ok_(1.0 not in self.zset) 33 | ok_(2.0 not in self.zset) 34 | eq_(1.0, self.zset["one"]) 35 | eq_(2.0, self.zset["two"]) 36 | with assert_raises(KeyError): 37 | self.zset[1.0] 38 | with assert_raises(KeyError): 39 | self.zset[2.0] 40 | eq_(0, self.zset.rank("one")) 41 | eq_(1, self.zset.rank("two")) 42 | eq_(None, self.zset.rank(1.0)) 43 | eq_(None, self.zset.rank(2.0)) 44 | 45 | # re-insert a value 46 | ok_(not self.zset.insert("one", 3.0)) 47 | 48 | # validate the update 49 | eq_(2, len(self.zset)) 50 | eq_(3.0, self.zset.score("one")) 51 | eq_(0, self.zset.rank("two")) 52 | eq_(1, self.zset.rank("one")) 53 | 54 | def test_remove(self): 55 | """ 56 | Removal maintains order. 57 | """ 58 | # insert a few elements 59 | self.zset["one"] = 1.0 60 | self.zset["uno"] = 1.0 61 | self.zset["three"] = 3.0 62 | self.zset["two"] = 2.0 63 | 64 | # cannot remove a member that is not present 65 | eq_(False, self.zset.remove("four")) 66 | 67 | # removing an existing entry works 68 | eq_(True, self.zset.remove("two")) 69 | eq_(3, len(self.zset)) 70 | eq_(0, self.zset.rank("one")) 71 | eq_(1, self.zset.rank("uno")) 72 | eq_(None, self.zset.rank("two")) 73 | eq_(2, self.zset.rank("three")) 74 | 75 | # delete also works 76 | del self.zset["uno"] 77 | eq_(2, len(self.zset)) 78 | eq_(0, self.zset.rank("one")) 79 | eq_(None, self.zset.rank("uno")) 80 | eq_(None, self.zset.rank("two")) 81 | eq_(1, self.zset.rank("three")) 82 | 83 | def test_scoremap(self): 84 | self.zset["one"] = 1.0 85 | self.zset["uno"] = 1.0 86 | self.zset["two"] = 2.0 87 | self.zset["three"] = 3.0 88 | eq_([(1.0, "one"), (1.0, "uno")], self.zset.scorerange(1.0, 1.1)) 89 | eq_([(1.0, "one"), (1.0, "uno"), (2.0, "two")], 90 | self.zset.scorerange(1.0, 2.0)) 91 | 92 | def test_scoremap_inclusive(self): 93 | self.zset["one"] = 1.0 94 | self.zset["uno"] = 1.0 95 | self.zset["uno_dot_one"] = 1.1 96 | self.zset["two"] = 2.0 97 | self.zset["three"] = 3.0 98 | eq_([], self.zset.scorerange(1.0, 1.1, start_inclusive=False, end_inclusive=False)) 99 | eq_([(1.0, "one"), (1.0, "uno")], self.zset.scorerange(1.0, 1.1, start_inclusive=True, end_inclusive=False)) # noqa 100 | eq_([(1.1, "uno_dot_one")], self.zset.scorerange(1.0, 1.1, start_inclusive=False, end_inclusive=True)) # noqa 101 | eq_([(1.1, "uno_dot_one")], self.zset.scorerange(1.0, 2.0, start_inclusive=False, end_inclusive=False)) # noqa 102 | eq_([(1.1, "uno_dot_one"), (2.0, "two")], 103 | self.zset.scorerange(1.0, 3.0, start_inclusive=False, end_inclusive=False)) 104 | 105 | eq_([(1.0, "one"), (1.0, "uno"), (1.1, "uno_dot_one")], 106 | self.zset.scorerange(1.0, 1.1, start_inclusive=True, end_inclusive=True)) 107 | eq_([(1.0, "one"), (1.0, "uno"), (1.1, "uno_dot_one"), (2.0, "two")], 108 | self.zset.scorerange(1.0, 2.0, start_inclusive=True, end_inclusive=True)) 109 | -------------------------------------------------------------------------------- /mockredis/tests/test_hash.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_, ok_ 2 | 3 | from mockredis.tests.fixtures import setup, teardown 4 | 5 | 6 | class TestRedisHash(object): 7 | """hash tests""" 8 | 9 | def setup(self): 10 | setup(self) 11 | 12 | def teardown(self): 13 | teardown(self) 14 | 15 | def test_hexists(self): 16 | hashkey = "hash" 17 | ok_(not self.redis.hexists(hashkey, "key")) 18 | self.redis.hset(hashkey, "key", "value") 19 | ok_(self.redis.hexists(hashkey, "key")) 20 | ok_(not self.redis.hexists(hashkey, "key2")) 21 | 22 | def test_hgetall(self): 23 | hashkey = "hash" 24 | eq_({}, self.redis.hgetall(hashkey)) 25 | self.redis.hset(hashkey, "key", "value") 26 | eq_({b"key": b"value"}, self.redis.hgetall(hashkey)) 27 | 28 | def test_hdel(self): 29 | hashkey = "hash" 30 | self.redis.hmset(hashkey, {1: 1, 2: 2, 3: 3}) 31 | eq_(0, self.redis.hdel(hashkey, "foo")) 32 | eq_({b"1": b"1", b"2": b"2", b"3": b"3"}, self.redis.hgetall(hashkey)) 33 | eq_(2, self.redis.hdel(hashkey, "1", 2)) 34 | eq_({b"3": b"3"}, self.redis.hgetall(hashkey)) 35 | eq_(1, self.redis.hdel(hashkey, "3", 4)) 36 | eq_({}, self.redis.hgetall(hashkey)) 37 | ok_(not self.redis.exists(hashkey)) 38 | eq_([], self.redis.keys("*")) 39 | 40 | def test_hlen(self): 41 | hashkey = "hash" 42 | eq_(0, self.redis.hlen(hashkey)) 43 | self.redis.hset(hashkey, "key", "value") 44 | eq_(1, self.redis.hlen(hashkey)) 45 | 46 | def test_hset(self): 47 | hashkey = "hash" 48 | eq_(1, self.redis.hset(hashkey, "key", "value")) 49 | eq_(b"value", self.redis.hget(hashkey, "key")) 50 | eq_(0, self.redis.hset(hashkey, "key", "value2")) 51 | 52 | def test_hget(self): 53 | hashkey = "hash" 54 | eq_(None, self.redis.hget(hashkey, "key")) 55 | 56 | def test_hset_integral(self): 57 | hashkey = "hash" 58 | eq_(1, self.redis.hset(hashkey, 1, 2)) 59 | eq_(b"2", self.redis.hget(hashkey, 1)) 60 | eq_(b"2", self.redis.hget(hashkey, "1")) 61 | 62 | def test_hsetnx(self): 63 | hashkey = "hash" 64 | eq_(1, self.redis.hsetnx(hashkey, "key", "value1")) 65 | eq_(b"value1", self.redis.hget(hashkey, "key")) 66 | eq_(0, self.redis.hsetnx(hashkey, "key", "value2")) 67 | eq_(b"value1", self.redis.hget(hashkey, "key")) 68 | 69 | def test_hmset(self): 70 | hashkey = "hash" 71 | eq_(True, self.redis.hmset(hashkey, {"key1": "value1", "key2": "value2"})) 72 | eq_(b"value1", self.redis.hget(hashkey, "key1")) 73 | eq_(b"value2", self.redis.hget(hashkey, "key2")) 74 | 75 | def test_hmset_integral(self): 76 | hashkey = "hash" 77 | self.redis.hmset(hashkey, {1: 2, 3: 4}) 78 | eq_(b"2", self.redis.hget(hashkey, "1")) 79 | eq_(b"2", self.redis.hget(hashkey, 1)) 80 | eq_(b"4", self.redis.hget(hashkey, "3")) 81 | eq_(b"4", self.redis.hget(hashkey, 3)) 82 | 83 | def test_hmget(self): 84 | hashkey = "hash" 85 | self.redis.hmset(hashkey, {1: 2, 3: 4}) 86 | eq_([b"2", None, b"4"], self.redis.hmget(hashkey, "1", "2", "3")) 87 | eq_([b"2", None, b"4"], self.redis.hmget(hashkey, ["1", "2", "3"])) 88 | eq_([b"2", None, b"4"], self.redis.hmget(hashkey, [1, 2, 3])) 89 | 90 | def test_hincrby(self): 91 | hashkey = "hash" 92 | eq_(1, self.redis.hincrby(hashkey, "key", 1)) 93 | eq_(3, self.redis.hincrby(hashkey, "key", 2)) 94 | eq_(b"3", self.redis.hget(hashkey, "key")) 95 | 96 | def test_hincrbyfloat(self): 97 | hashkey = "hash" 98 | eq_(1.2, self.redis.hincrbyfloat(hashkey, "key", 1.2)) 99 | eq_(3.5, self.redis.hincrbyfloat(hashkey, "key", 2.3)) 100 | eq_(b"3.5", self.redis.hget(hashkey, "key")) 101 | 102 | def test_hkeys(self): 103 | hashkey = "hash" 104 | self.redis.hmset(hashkey, {1: 2, 3: 4}) 105 | eq_([b"1", b"3"], sorted(self.redis.hkeys(hashkey))) 106 | 107 | def test_hvals(self): 108 | hashkey = "hash" 109 | self.redis.hmset(hashkey, {1: 2, 3: 4}) 110 | eq_([b"2", b"4"], sorted(self.redis.hvals(hashkey))) 111 | -------------------------------------------------------------------------------- /mockredis/sortedset.py: -------------------------------------------------------------------------------- 1 | from bisect import bisect_left, bisect_right 2 | 3 | 4 | class SortedSet(object): 5 | """ 6 | Redis-style SortedSet implementation. 7 | 8 | Maintains two internal data structures: 9 | 10 | 1. A multimap from score to member 11 | 2. A dictionary from member to score. 12 | 13 | The multimap is implemented using a sorted list of (score, member) pairs. The bisect 14 | operations used to maintain the multimap are O(log N), but insertion into and removal 15 | from a list are O(N), so insertion and removal O(N). It should be possible to swap in 16 | an indexable skip list to get the expected O(log N) behavior. 17 | """ 18 | def __init__(self): 19 | """ 20 | Create an empty sorted set. 21 | """ 22 | # sorted list of (score, member) 23 | self._scores = [] 24 | # dictionary from member to score 25 | self._members = {} 26 | 27 | def clear(self): 28 | """ 29 | Remove all members and scores from the sorted set. 30 | """ 31 | self.__init__() 32 | 33 | def __len__(self): 34 | return len(self._members) 35 | 36 | def __contains__(self, member): 37 | return member in self._members 38 | 39 | def __str__(self): 40 | return self.__repr__() 41 | 42 | def __repr__(self): 43 | return "SortedSet({})".format(self._scores) 44 | 45 | def __eq__(self, other): 46 | return self._scores == other._scores and self._members == other._members 47 | 48 | def __ne__(self, other): 49 | return not self == other 50 | 51 | def __setitem__(self, member, score): 52 | """ 53 | Insert member with score. If member is already present in the 54 | set, update its score. 55 | """ 56 | self.insert(member, score) 57 | 58 | def __delitem__(self, member): 59 | """ 60 | Remove member from the set. 61 | """ 62 | self.remove(member) 63 | 64 | def __getitem__(self, member): 65 | """ 66 | Get the score for a member. 67 | """ 68 | if isinstance(member, slice): 69 | raise TypeError("Slicing not supported") 70 | return self._members[member] 71 | 72 | def __iter__(self): 73 | return self._scores.__iter__() 74 | 75 | def __reversed__(self): 76 | return self._scores.__reversed__() 77 | 78 | def insert(self, member, score): 79 | """ 80 | Identical to __setitem__, but returns whether a member was 81 | inserted (True) or updated (False) 82 | """ 83 | found = self.remove(member) 84 | index = bisect_left(self._scores, (score, member)) 85 | self._scores.insert(index, (score, member)) 86 | self._members[member] = score 87 | return not found 88 | 89 | def remove(self, member): 90 | """ 91 | Identical to __delitem__, but returns whether a member was removed. 92 | """ 93 | if member not in self: 94 | return False 95 | score = self._members[member] 96 | score_index = bisect_left(self._scores, (score, member)) 97 | del self._scores[score_index] 98 | del self._members[member] 99 | return True 100 | 101 | def score(self, member): 102 | """ 103 | Identical to __getitem__, but returns None instead of raising 104 | KeyError if member is not found. 105 | """ 106 | return self._members.get(member) 107 | 108 | def rank(self, member): 109 | """ 110 | Get the rank (index of a member). 111 | """ 112 | score = self._members.get(member) 113 | if score is None: 114 | return None 115 | return bisect_left(self._scores, (score, member)) 116 | 117 | def range(self, start, end, desc=False): 118 | """ 119 | Return (score, member) pairs between min and max ranks. 120 | """ 121 | if not self: 122 | return [] 123 | 124 | if desc: 125 | return reversed(self._scores[len(self) - end - 1:len(self) - start]) 126 | else: 127 | return self._scores[start:end + 1] 128 | 129 | def scorerange(self, start, end, start_inclusive=True, end_inclusive=True): 130 | """ 131 | Return (score, member) pairs between min and max scores. 132 | """ 133 | if not self: 134 | return [] 135 | 136 | left = bisect_left(self._scores, (start,)) 137 | right = bisect_right(self._scores, (end,)) 138 | 139 | if end_inclusive: 140 | # end is inclusive 141 | while right < len(self) and self._scores[right][0] == end: 142 | right += 1 143 | 144 | if not start_inclusive: 145 | while left < right and self._scores[left][0] == start: 146 | left += 1 147 | return self._scores[left:right] 148 | 149 | def min_score(self): 150 | return self._scores[0][0] 151 | 152 | def max_score(self): 153 | return self._scores[-1][0] 154 | -------------------------------------------------------------------------------- /mockredis/tests/test_pipeline.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha1 2 | 3 | from nose.tools import eq_ 4 | 5 | from mockredis.tests.fixtures import (assert_raises_redis_error, 6 | assert_raises_watch_error, 7 | setup, 8 | teardown) 9 | 10 | 11 | class TestPipeline(object): 12 | 13 | def setup(self): 14 | setup(self) 15 | 16 | def teardown(self): 17 | teardown(self) 18 | 19 | def test_pipeline(self): 20 | """ 21 | Pipeline execution returns all of the saved up values. 22 | """ 23 | with self.redis.pipeline() as pipeline: 24 | pipeline.echo("foo") 25 | pipeline.echo("bar") 26 | 27 | eq_([b"foo", b"bar"], pipeline.execute()) 28 | 29 | def test_pipeline_args(self): 30 | """ 31 | It should be possible to pass transaction and shard_hint. 32 | """ 33 | with self.redis.pipeline(transaction=False, shard_hint=None): 34 | pass 35 | 36 | def test_transaction(self): 37 | self.redis["a"] = 1 38 | self.redis["b"] = 2 39 | has_run = [] 40 | 41 | def my_transaction(pipe): 42 | a_value = pipe.get("a") 43 | assert a_value in (b"1", b"2") 44 | b_value = pipe.get("b") 45 | assert b_value == b"2" 46 | 47 | # silly run-once code... incr's "a" so WatchError should be raised 48 | # forcing this all to run again. this should incr "a" once to "2" 49 | if not has_run: 50 | self.redis.incr("a") 51 | has_run.append(True) 52 | 53 | pipe.multi() 54 | pipe.set("c", int(a_value) + int(b_value)) 55 | 56 | result = self.redis.transaction(my_transaction, "a", "b") 57 | eq_([True], result) 58 | eq_(b"4", self.redis["c"]) 59 | 60 | def test_set_and_get(self): 61 | """ 62 | Pipeline execution returns the pipeline, not the intermediate value. 63 | """ 64 | with self.redis.pipeline() as pipeline: 65 | eq_(pipeline, pipeline.set("foo", "bar")) 66 | eq_(pipeline, pipeline.get("foo")) 67 | 68 | eq_([True, b"bar"], pipeline.execute()) 69 | 70 | def test_scripts(self): 71 | """ 72 | Verify that script calls work across pipelines. 73 | 74 | This test basically ensures that the pipeline shares 75 | state with the mock redis instance. 76 | """ 77 | script_content = "redis.call('PING')" 78 | sha = sha1(script_content.encode("utf-8")).hexdigest() 79 | 80 | script_sha = self.redis.script_load(script_content) 81 | eq_(script_sha, sha) 82 | 83 | # Script exists in mock redis 84 | eq_([True], self.redis.script_exists(sha)) 85 | 86 | # Script exists in pipeline 87 | eq_([True], self.redis.pipeline().script_exists(sha).execute()[0]) 88 | 89 | def test_watch(self): 90 | """ 91 | Verify watch puts the pipeline in immediate execution mode. 92 | """ 93 | with self.redis.pipeline() as pipeline: 94 | pipeline.watch("key1", "key2") 95 | eq_(None, pipeline.get("key1")) 96 | eq_(None, pipeline.get("key2")) 97 | eq_(True, pipeline.set("foo", "bar")) 98 | eq_(b"bar", pipeline.get("foo")) 99 | 100 | def test_multi(self): 101 | """ 102 | Test explicit transaction with multi command. 103 | """ 104 | with self.redis.pipeline() as pipeline: 105 | pipeline.multi() 106 | eq_(pipeline, pipeline.set("foo", "bar")) 107 | eq_(pipeline, pipeline.get("foo")) 108 | 109 | eq_([True, b"bar"], pipeline.execute()) 110 | 111 | def test_multi_with_watch(self): 112 | """ 113 | Test explicit transaction with watched keys. 114 | """ 115 | self.redis.set("foo", "bar") 116 | 117 | with self.redis.pipeline() as pipeline: 118 | pipeline.watch("foo") 119 | eq_(b"bar", pipeline.get("foo")) 120 | 121 | pipeline.multi() 122 | eq_(pipeline, pipeline.set("foo", "baz")) 123 | eq_(pipeline, pipeline.get("foo")) 124 | 125 | eq_([True, b"baz"], pipeline.execute()) 126 | 127 | def test_multi_with_watch_zset(self): 128 | """ 129 | Test explicit transaction with watched keys, this time with zset 130 | """ 131 | self.redis.zadd("foo", "bar", 1.0) 132 | 133 | with self.redis.pipeline() as pipeline: 134 | pipeline.watch("foo") 135 | eq_(1, pipeline.zcard("foo")) 136 | pipeline.multi() 137 | eq_(pipeline, pipeline.zadd("foo", "baz", 2.0)) 138 | eq_([1], pipeline.execute()) 139 | 140 | def test_multi_with_watch_error(self): 141 | """ 142 | Test explicit transaction with watched keys. 143 | """ 144 | with self.redis.pipeline() as pipeline: 145 | pipeline.watch("foo") 146 | eq_(True, pipeline.set("foo", "bar")) 147 | eq_(b"bar", pipeline.get("foo")) 148 | 149 | pipeline.multi() 150 | eq_(pipeline, pipeline.set("foo", "baz")) 151 | eq_(pipeline, pipeline.get("foo")) 152 | 153 | with assert_raises_watch_error(): 154 | eq_([True, b"baz"], pipeline.execute()) 155 | 156 | def test_watch_after_multi(self): 157 | """ 158 | Cannot watch after multi. 159 | """ 160 | with self.redis.pipeline() as pipeline: 161 | pipeline.multi() 162 | with assert_raises_redis_error(): 163 | pipeline.watch() 164 | 165 | def test_multiple_multi_calls(self): 166 | """ 167 | Cannot call multi mutliple times. 168 | """ 169 | with self.redis.pipeline() as pipeline: 170 | pipeline.multi() 171 | with assert_raises_redis_error(): 172 | pipeline.multi() 173 | 174 | def test_multi_on_implicit_transaction(self): 175 | """ 176 | Cannot start an explicit transaction when commands have already been issued. 177 | """ 178 | with self.redis.pipeline() as pipeline: 179 | pipeline.set("foo", "bar") 180 | with assert_raises_redis_error(): 181 | pipeline.multi() 182 | -------------------------------------------------------------------------------- /mockredis/script.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | from mockredis.exceptions import ResponseError 4 | 5 | LuaLock = threading.Lock() 6 | 7 | 8 | class Script(object): 9 | """ 10 | An executable Lua script object returned by ``MockRedis.register_script``. 11 | """ 12 | 13 | def __init__(self, registered_client, script, load_dependencies=True): 14 | self.registered_client = registered_client 15 | self.script = script 16 | self.load_dependencies = load_dependencies 17 | self.sha = registered_client.script_load(script) 18 | 19 | def __call__(self, keys=[], args=[], client=None): 20 | """Execute the script, passing any required ``args``""" 21 | with LuaLock: 22 | client = client or self.registered_client 23 | 24 | if not client.script_exists(self.sha)[0]: 25 | self.sha = client.script_load(self.script) 26 | 27 | return self._execute_lua(keys, args, client) 28 | 29 | def _execute_lua(self, keys, args, client): 30 | """ 31 | Sets KEYS and ARGV alongwith redis.call() function in lua globals 32 | and executes the lua redis script 33 | """ 34 | lua, lua_globals = Script._import_lua(self.load_dependencies) 35 | lua_globals.KEYS = self._python_to_lua(keys) 36 | lua_globals.ARGV = self._python_to_lua(args) 37 | 38 | def _call(*call_args): 39 | # redis-py and native redis commands are mostly compatible argument 40 | # wise, but some exceptions need to be handled here: 41 | if str(call_args[0]).lower() == 'lrem': 42 | response = client.call( 43 | call_args[0], call_args[1], 44 | call_args[3], # "count", default is 0 45 | call_args[2]) 46 | else: 47 | response = client.call(*call_args) 48 | return self._python_to_lua(response) 49 | 50 | lua_globals.redis = {"call": _call} 51 | return self._lua_to_python(lua.execute(self.script), return_status=True) 52 | 53 | @staticmethod 54 | def _import_lua(load_dependencies=True): 55 | """ 56 | Import lua and dependencies. 57 | 58 | :param load_dependencies: should Lua library dependencies be loaded? 59 | :raises: RuntimeError if Lua is not available 60 | """ 61 | try: 62 | import lua 63 | except ImportError: 64 | raise RuntimeError("Lua not installed") 65 | 66 | lua_globals = lua.globals() 67 | if load_dependencies: 68 | Script._import_lua_dependencies(lua, lua_globals) 69 | return lua, lua_globals 70 | 71 | @staticmethod 72 | def _import_lua_dependencies(lua, lua_globals): 73 | """ 74 | Imports lua dependencies that are supported by redis lua scripts. 75 | 76 | The current implementation is fragile to the target platform and lua version 77 | and may be disabled if these imports are not needed. 78 | 79 | Included: 80 | - cjson lib. 81 | Pending: 82 | - base lib. 83 | - table lib. 84 | - string lib. 85 | - math lib. 86 | - debug lib. 87 | - cmsgpack lib. 88 | """ 89 | if sys.platform not in ('darwin', 'windows'): 90 | import ctypes 91 | ctypes.CDLL('liblua5.2.so', mode=ctypes.RTLD_GLOBAL) 92 | 93 | try: 94 | lua_globals.cjson = lua.eval('require "cjson"') 95 | except RuntimeError: 96 | raise RuntimeError("cjson not installed") 97 | 98 | @staticmethod 99 | def _lua_to_python(lval, return_status=False): 100 | """ 101 | Convert Lua object(s) into Python object(s), as at times Lua object(s) 102 | are not compatible with Python functions 103 | """ 104 | import lua 105 | lua_globals = lua.globals() 106 | if lval is None: 107 | # Lua None --> Python None 108 | return None 109 | if lua_globals.type(lval) == "table": 110 | # Lua table --> Python list 111 | pval = [] 112 | for i in lval: 113 | if return_status: 114 | if i == 'ok': 115 | return lval[i] 116 | if i == 'err': 117 | raise ResponseError(lval[i]) 118 | pval.append(Script._lua_to_python(lval[i])) 119 | return pval 120 | elif isinstance(lval, long): 121 | # Lua number --> Python long 122 | return long(lval) 123 | elif isinstance(lval, float): 124 | # Lua number --> Python float 125 | return float(lval) 126 | elif lua_globals.type(lval) == "userdata": 127 | # Lua userdata --> Python string 128 | return str(lval) 129 | elif lua_globals.type(lval) == "string": 130 | # Lua string --> Python string 131 | return lval 132 | elif lua_globals.type(lval) == "boolean": 133 | # Lua boolean --> Python bool 134 | return bool(lval) 135 | raise RuntimeError("Invalid Lua type: " + str(lua_globals.type(lval))) 136 | 137 | @staticmethod 138 | def _python_to_lua(pval): 139 | """ 140 | Convert Python object(s) into Lua object(s), as at times Python object(s) 141 | are not compatible with Lua functions 142 | """ 143 | import lua 144 | if pval is None: 145 | # Python None --> Lua None 146 | return lua.eval("") 147 | if isinstance(pval, (list, tuple, set)): 148 | # Python list --> Lua table 149 | # e.g.: in lrange 150 | # in Python returns: [v1, v2, v3] 151 | # in Lua returns: {v1, v2, v3} 152 | lua_list = lua.eval("{}") 153 | lua_table = lua.eval("table") 154 | for item in pval: 155 | lua_table.insert(lua_list, Script._python_to_lua(item)) 156 | return lua_list 157 | elif isinstance(pval, dict): 158 | # Python dict --> Lua dict 159 | # e.g.: in hgetall 160 | # in Python returns: {k1:v1, k2:v2, k3:v3} 161 | # in Lua returns: {k1, v1, k2, v2, k3, v3} 162 | lua_dict = lua.eval("{}") 163 | lua_table = lua.eval("table") 164 | for k, v in pval.iteritems(): 165 | lua_table.insert(lua_dict, Script._python_to_lua(k)) 166 | lua_table.insert(lua_dict, Script._python_to_lua(v)) 167 | return lua_dict 168 | elif isinstance(pval, str): 169 | # Python string --> Lua userdata 170 | return pval 171 | elif isinstance(pval, bool): 172 | # Python bool--> Lua boolean 173 | return lua.eval(str(pval).lower()) 174 | elif isinstance(pval, (int, long, float)): 175 | # Python int --> Lua number 176 | lua_globals = lua.globals() 177 | return lua_globals.tonumber(str(pval)) 178 | 179 | raise RuntimeError("Invalid Python type: " + str(type(pval))) 180 | -------------------------------------------------------------------------------- /mockredis/tests/test_redis.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from time import time 3 | import sys 4 | 5 | from nose.tools import assert_raises, eq_, ok_ 6 | 7 | from mockredis.tests.fixtures import setup, teardown 8 | 9 | if sys.version_info >= (3, 0): 10 | long = int 11 | 12 | 13 | class TestRedis(object): 14 | 15 | def setup(self): 16 | setup(self) 17 | 18 | def teardown(self): 19 | teardown(self) 20 | 21 | def test_get_types(self): 22 | ''' 23 | testing type conversions for set/get, hset/hget, sadd/smembers 24 | 25 | Python bools, lists, dicts are returned as strings by 26 | redis-py/redis. 27 | ''' 28 | 29 | values = list([ 30 | True, 31 | False, 32 | [1, '2'], 33 | { 34 | 'a': 1, 35 | 'b': 'c' 36 | }, 37 | ]) 38 | 39 | eq_(None, self.redis.get('key')) 40 | 41 | for value in values: 42 | self.redis.set('key', value) 43 | eq_(str(value).encode('utf8'), 44 | self.redis.get('key')) 45 | 46 | self.redis.hset('hkey', 'item', value) 47 | eq_(str(value).encode('utf8'), 48 | self.redis.hget('hkey', 'item')) 49 | 50 | self.redis.sadd('skey', value) 51 | eq_(set([str(value).encode('utf8')]), 52 | self.redis.smembers('skey')) 53 | 54 | self.redis.flushdb() 55 | 56 | def test_incr(self): 57 | ''' 58 | incr, hincr when keys exist 59 | ''' 60 | 61 | values = list([ 62 | (1, b'2'), 63 | ('1', b'2'), 64 | ]) 65 | 66 | for value in values: 67 | self.redis.set('key', value[0]) 68 | self.redis.incr('key') 69 | eq_(value[1], 70 | self.redis.get('key'), 71 | "redis.incr") 72 | 73 | self.redis.hset('hkey', 'attr', value[0]) 74 | self.redis.hincrby('hkey', 'attr') 75 | eq_(value[1], 76 | self.redis.hget('hkey', 'attr'), 77 | "redis.hincrby") 78 | 79 | self.redis.flushdb() 80 | 81 | def test_incr_init(self): 82 | ''' 83 | incr, hincr, decr when keys do NOT exist 84 | ''' 85 | 86 | self.redis.incr('key') 87 | eq_(b'1', self.redis.get('key')) 88 | 89 | self.redis.hincrby('hkey', 'attr') 90 | eq_(b'1', self.redis.hget('hkey', 'attr')) 91 | 92 | self.redis.decr('dkey') 93 | eq_(b'-1', self.redis.get('dkey')) 94 | 95 | def test_ttl(self): 96 | self.redis.set('key', 'key') 97 | self.redis.expire('key', 30) 98 | 99 | result = self.redis.ttl('key') 100 | ok_(isinstance(result, long)) 101 | # should be less than the timeout originally set 102 | ok_(result <= 30) 103 | 104 | def test_ttl_timedelta(self): 105 | self.redis.set('key', 'key') 106 | self.redis.expire('key', timedelta(seconds=30)) 107 | 108 | result = self.redis.ttl('key') 109 | ok_(isinstance(result, long)) 110 | # should be less than the timeout originally set 111 | ok_(result <= 30) 112 | 113 | def test_ttl_when_absent(self): 114 | """ 115 | Test absent ttl handling. 116 | """ 117 | eq_(self.redis.ttl("invalid_key"), None) 118 | self.redis.set("key", "value") 119 | eq_(self.redis.ttl("key"), None) 120 | 121 | # redis >= 2.8.0 returns -2 if the key does exist 122 | eq_(self.redis_strict.ttl("invalid_key"), -2) 123 | # redis >= 2.8.0 returns -1 if there is no ttl 124 | self.redis_strict.set("key", "value") 125 | eq_(self.redis_strict.ttl("key"), -1) 126 | 127 | def test_ttl_no_timeout(self): 128 | """ 129 | Test whether, like the redis-py lib, ttl returns None if the key has no timeout set. 130 | """ 131 | self.redis.set('key', 'key') 132 | eq_(self.redis.ttl('key'), None) 133 | 134 | def test_pttl(self): 135 | expiration_ms = 3000 136 | self.redis.set('key', 'key') 137 | self.redis.pexpire('key', expiration_ms) 138 | 139 | result = self.redis.pttl('key') 140 | ok_(isinstance(result, long)) 141 | # should be less than the timeout originally set 142 | ok_(result <= expiration_ms) 143 | 144 | def test_pttl_when_absent(self): 145 | """ 146 | Test absent pttl handling. 147 | """ 148 | # redis >= 2.8.0 returns -2 if the key does exist 149 | eq_(self.redis.pttl("invalid_key"), None) 150 | eq_(self.redis_strict.pttl("invalid_key"), -2) 151 | 152 | # redis >= 2.8.0 returns -1 if there is no ttl 153 | self.redis.set("key", "value") 154 | eq_(self.redis.pttl("key"), None) 155 | self.redis_strict.set("key", "value") 156 | eq_(self.redis_strict.pttl("key"), -1) 157 | 158 | def test_pttl_no_timeout(self): 159 | """ 160 | Test whether, like the redis-py lib, pttl returns None if the key has no timeout set. 161 | """ 162 | self.redis.set('key', 'key') 163 | eq_(self.redis.pttl('key'), None) 164 | 165 | def test_expireat_calculates_time(self): 166 | """ 167 | test whether expireat sets the correct ttl, setting a timestamp 30s in the future 168 | """ 169 | self.redis.set('key', 'key') 170 | self.redis.expireat('key', int(time()) + 30) 171 | 172 | result = self.redis.ttl('key') 173 | ok_(isinstance(result, long)) 174 | # should be less than the timeout originally set 175 | ok_(result <= 30, "Expected {} to be less than 30".format(result)) 176 | 177 | def test_keys(self): 178 | eq_([], self.redis.keys("*")) 179 | 180 | self.redis.set("foo", "bar") 181 | eq_([b"foo"], self.redis.keys("*")) 182 | eq_([b"foo"], self.redis.keys("foo*")) 183 | eq_([b"foo"], self.redis.keys("foo")) 184 | eq_([], self.redis.keys("bar")) 185 | 186 | self.redis.set("food", "bbq") 187 | eq_({b"foo", b"food"}, set(self.redis.keys("*"))) 188 | eq_({b"foo", b"food"}, set(self.redis.keys("foo*"))) 189 | eq_([b"foo"], self.redis.keys("foo")) 190 | eq_([b"food"], self.redis.keys("food")) 191 | eq_([], self.redis.keys("bar")) 192 | 193 | def test_keys_unicode(self): 194 | eq_([], self.redis.keys("*")) 195 | 196 | # This is a little backwards, but python3.2 has trouble with unicode in strings. 197 | key_as_utf8 = b'eat \xf0\x9f\x8d\xb0 now' 198 | key = key_as_utf8.decode('utf-8') 199 | self.redis.set(key, "bar") 200 | eq_([key_as_utf8], self.redis.keys("*")) 201 | eq_([key_as_utf8], self.redis.keys("eat*")) 202 | eq_([key_as_utf8], self.redis.keys("[ea]at * n?[a-z]")) 203 | 204 | unicode_prefix = b'eat \xf0\x9f\x8d\xb0*'.decode('utf-8') 205 | eq_([key_as_utf8], self.redis.keys(unicode_prefix)) 206 | eq_([key_as_utf8], self.redis.keys(unicode_prefix.encode('utf-8'))) 207 | unicode_prefix = b'eat \xf0\x9f\x8d\xb1*'.decode('utf-8') 208 | eq_([], self.redis.keys(unicode_prefix)) 209 | 210 | def test_contains(self): 211 | ok_("foo" not in self.redis) 212 | self.redis.set("foo", "bar") 213 | ok_("foo" in self.redis) 214 | 215 | def test_getitem(self): 216 | with assert_raises(KeyError): 217 | self.redis["foo"] 218 | self.redis.set("foo", "bar") 219 | eq_(b"bar", self.redis["foo"]) 220 | self.redis.delete("foo") 221 | with assert_raises(KeyError): 222 | self.redis["foo"] 223 | 224 | def test_setitem(self): 225 | eq_(None, self.redis.get("foo")) 226 | self.redis["foo"] = "bar" 227 | eq_(b"bar", self.redis.get("foo")) 228 | 229 | def test_delitem(self): 230 | self.redis["foo"] = "bar" 231 | eq_(b"bar", self.redis["foo"]) 232 | del self.redis["foo"] 233 | eq_(None, self.redis.get("foo")) 234 | # redispy does not correctly raise KeyError here, so we don't either 235 | del self.redis["foo"] 236 | 237 | def test_rename(self): 238 | self.redis["foo"] = "bar" 239 | ok_(self.redis.rename("foo", "new_foo")) 240 | eq_(b"bar", self.redis.get("new_foo")) 241 | 242 | def test_renamenx(self): 243 | self.redis["foo"] = "bar" 244 | self.redis["foo2"] = "bar2" 245 | eq_(self.redis.renamenx("foo", "foo2"), 0) 246 | eq_(b"bar2", self.redis.get("foo2")) 247 | eq_(self.redis.renamenx("foo", "foo3"), 1) 248 | eq_(b"bar", self.redis.get("foo3")) 249 | 250 | def test_dbsize(self): 251 | self.redis["foo"] = "bar" 252 | eq_(1, self.redis.dbsize()) 253 | del self.redis["foo"] 254 | eq_(0, self.redis.dbsize()) 255 | -------------------------------------------------------------------------------- /mockredis/tests/test_set.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_raises, eq_, ok_ 2 | 3 | from mockredis.exceptions import ResponseError 4 | from mockredis.tests.fixtures import setup, teardown 5 | 6 | 7 | class TestRedisSet(object): 8 | """set tests""" 9 | 10 | def setup(self): 11 | setup(self) 12 | 13 | def teardown(self): 14 | teardown(self) 15 | 16 | def test_sadd_empty(self): 17 | key = "set" 18 | values = [] 19 | with assert_raises(ResponseError): 20 | self.redis.sadd(key, *values) 21 | 22 | def test_sadd(self): 23 | key = "set" 24 | values = ["one", "uno", "two", "three"] 25 | for value in values: 26 | eq_(1, self.redis.sadd(key, value)) 27 | 28 | def test_sadd_multiple(self): 29 | key = "set" 30 | values = ["one", "uno", "two", "three"] 31 | eq_(4, self.redis.sadd(key, *values)) 32 | 33 | def test_sadd_duplicate_key(self): 34 | key = "set" 35 | eq_(1, self.redis.sadd(key, "one")) 36 | eq_(0, self.redis.sadd(key, "one")) 37 | 38 | def test_scard(self): 39 | key = "set" 40 | eq_(0, self.redis.scard(key)) 41 | ok_(key not in self.redis) 42 | values = ["one", "uno", "two", "three"] 43 | eq_(4, self.redis.sadd(key, *values)) 44 | eq_(4, self.redis.scard(key)) 45 | 46 | def test_sdiff(self): 47 | self.redis.sadd("x", "one", "two", "three") 48 | self.redis.sadd("y", "one") 49 | self.redis.sadd("z", "two") 50 | 51 | with assert_raises(Exception): 52 | self.redis.sdiff([]) 53 | 54 | eq_(set(), self.redis.sdiff("w")) 55 | eq_(set([b"one", b"two", b"three"]), self.redis.sdiff("x")) 56 | eq_(set([b"two", b"three"]), self.redis.sdiff("x", "y")) 57 | eq_(set([b"two", b"three"]), self.redis.sdiff(["x", "y"])) 58 | eq_(set([b"three"]), self.redis.sdiff("x", "y", "z")) 59 | eq_(set([b"three"]), self.redis.sdiff(["x", "y"], "z")) 60 | 61 | def test_sdiffstore(self): 62 | self.redis.sadd("x", "one", "two", "three") 63 | self.redis.sadd("y", "one") 64 | self.redis.sadd("z", "two") 65 | 66 | with assert_raises(Exception): 67 | self.redis.sdiffstore("w", []) 68 | 69 | eq_(3, self.redis.sdiffstore("w", "x")) 70 | eq_(set([b"one", b"two", b"three"]), self.redis.smembers("w")) 71 | 72 | eq_(2, self.redis.sdiffstore("w", "x", "y")) 73 | eq_(set([b"two", b"three"]), self.redis.smembers("w")) 74 | eq_(2, self.redis.sdiffstore("w", ["x", "y"])) 75 | eq_(set([b"two", b"three"]), self.redis.smembers("w")) 76 | eq_(1, self.redis.sdiffstore("w", "x", "y", "z")) 77 | eq_(set([b"three"]), self.redis.smembers("w")) 78 | eq_(1, self.redis.sdiffstore("w", ["x", "y"], "z")) 79 | eq_(set([b"three"]), self.redis.smembers("w")) 80 | 81 | def test_sinter(self): 82 | self.redis.sadd("x", "one", "two", "three") 83 | self.redis.sadd("y", "one") 84 | self.redis.sadd("z", "two") 85 | 86 | with assert_raises(Exception): 87 | self.redis.sinter([]) 88 | 89 | eq_(set(), self.redis.sinter("w")) 90 | eq_(set([b"one", b"two", b"three"]), self.redis.sinter("x")) 91 | eq_(set([b"one"]), self.redis.sinter("x", "y")) 92 | eq_(set([b"two"]), self.redis.sinter(["x", "z"])) 93 | eq_(set(), self.redis.sinter("x", "y", "z")) 94 | eq_(set(), self.redis.sinter(["x", "y"], "z")) 95 | 96 | def test_sinterstore(self): 97 | self.redis.sadd("x", "one", "two", "three") 98 | self.redis.sadd("y", "one") 99 | self.redis.sadd("z", "two") 100 | 101 | with assert_raises(Exception): 102 | self.redis.sinterstore("w", []) 103 | 104 | eq_(3, self.redis.sinterstore("w", "x")) 105 | eq_(set([b"one", b"two", b"three"]), self.redis.smembers("w")) 106 | 107 | eq_(1, self.redis.sinterstore("w", "x", "y")) 108 | eq_(set([b"one"]), self.redis.smembers("w")) 109 | eq_(1, self.redis.sinterstore("w", ["x", "z"])) 110 | eq_(set([b"two"]), self.redis.smembers("w")) 111 | eq_(0, self.redis.sinterstore("w", "x", "y", "z")) 112 | eq_(set(), self.redis.smembers("w")) 113 | eq_(0, self.redis.sinterstore("w", ["x", "y"], "z")) 114 | eq_(set(), self.redis.smembers("w")) 115 | 116 | def test_sismember(self): 117 | key = "set" 118 | ok_(not self.redis.sismember(key, "one")) 119 | ok_(key not in self.redis) 120 | 121 | eq_(1, self.redis.sadd(key, "one")) 122 | ok_(self.redis.sismember(key, "one")) 123 | ok_(not self.redis.sismember(key, "two")) 124 | eq_(0, self.redis.sismember(key, "two")) 125 | 126 | def test_ismember_numeric(self): 127 | """ 128 | Verify string conversion. 129 | """ 130 | key = "set" 131 | eq_(1, self.redis.sadd(key, 1)) 132 | eq_(set([b"1"]), self.redis.smembers(key)) 133 | ok_(self.redis.sismember(key, "1")) 134 | ok_(self.redis.sismember(key, 1)) 135 | 136 | def test_smembers(self): 137 | key = "set" 138 | eq_(set(), self.redis.smembers(key)) 139 | ok_(key not in self.redis) 140 | eq_(1, self.redis.sadd(key, "one")) 141 | eq_(set([b"one"]), self.redis.smembers(key)) 142 | eq_(1, self.redis.sadd(key, "two")) 143 | eq_(set([b"one", b"two"]), self.redis.smembers(key)) 144 | 145 | def test_smembers_copy(self): 146 | key = "set" 147 | self.redis.sadd(key, "one", "two", "three") 148 | members = self.redis.smembers(key) 149 | eq_({b"one", b"two", b"three"}, members) 150 | for member in members: 151 | # Checking that SMEMBERS returns the copy of internal data structure instead of 152 | # direct references. Otherwise SREM operation may give following error. 153 | # RuntimeError: Set changed size during iteration 154 | self.redis.srem(key, member) 155 | eq_(set(), self.redis.smembers(key)) 156 | 157 | def test_smove(self): 158 | eq_(0, self.redis.smove("x", "y", "one")) 159 | 160 | eq_(2, self.redis.sadd("x", "one", "two")) 161 | eq_(set([b"one", b"two"]), self.redis.smembers("x")) 162 | eq_(set(), self.redis.smembers("y")) 163 | 164 | eq_(0, self.redis.smove("x", "y", "three")) 165 | eq_(set([b"one", b"two"]), self.redis.smembers("x")) 166 | eq_(set(), self.redis.smembers("y")) 167 | 168 | eq_(1, self.redis.smove("x", "y", "one")) 169 | eq_(set([b"two"]), self.redis.smembers("x")) 170 | eq_(set([b"one"]), self.redis.smembers("y")) 171 | 172 | def test_spop(self): 173 | key = "set" 174 | eq_(None, self.redis.spop(key)) 175 | eq_(1, self.redis.sadd(key, "one")) 176 | eq_(b"one", self.redis.spop(key)) 177 | eq_(0, self.redis.scard(key)) 178 | eq_(1, self.redis.sadd(key, "one")) 179 | eq_(1, self.redis.sadd(key, "two")) 180 | first = self.redis.spop(key) 181 | ok_(first in [b"one", b"two"]) 182 | eq_(1, self.redis.scard(key)) 183 | second = self.redis.spop(key) 184 | eq_(b"one" if first == b"two" else b"two", second) 185 | eq_(0, self.redis.scard(key)) 186 | eq_([], self.redis.keys("*")) 187 | 188 | def test_srandmember(self): 189 | key = "set" 190 | # count is None 191 | eq_(None, self.redis.srandmember(key)) 192 | eq_(1, self.redis.sadd(key, "one")) 193 | eq_(b"one", self.redis.srandmember(key)) 194 | eq_(1, self.redis.scard(key)) 195 | eq_(1, self.redis.sadd(key, "two")) 196 | ok_(self.redis.srandmember(key) in [b"one", b"two"]) 197 | eq_(2, self.redis.scard(key)) 198 | # count > 0 199 | eq_([], self.redis.srandmember("empty", 1)) 200 | ok_(self.redis.srandmember(key, 1)[0] in [b"one", b"two"]) 201 | eq_(set([b"one", b"two"]), set(self.redis.srandmember(key, 2))) 202 | # count < 0 203 | eq_([], self.redis.srandmember("empty", -1)) 204 | ok_(self.redis.srandmember(key, -1)[0] in [b"one", b"two"]) 205 | members = self.redis.srandmember(key, -2) 206 | eq_(2, len(members)) 207 | for member in members: 208 | ok_(member in [b"one", b"two"]) 209 | 210 | def test_srem(self): 211 | key = "set" 212 | eq_(0, self.redis.srem(key, "one")) 213 | eq_(3, self.redis.sadd(key, "one", "two", "three")) 214 | eq_(0, self.redis.srem(key, "four")) 215 | eq_(2, self.redis.srem(key, "one", "three")) 216 | eq_(1, self.redis.srem(key, "two", "four")) 217 | eq_([], self.redis.keys("*")) 218 | 219 | def test_sunion(self): 220 | self.redis.sadd("x", "one", "two", "three") 221 | self.redis.sadd("y", "one") 222 | self.redis.sadd("z", "two") 223 | 224 | with assert_raises(Exception): 225 | self.redis.sunion([]) 226 | 227 | eq_(set(), self.redis.sunion("v")) 228 | eq_(set([b"one", b"two", b"three"]), self.redis.sunion("x")) 229 | eq_(set([b"one"]), self.redis.sunion("v", "y")) 230 | eq_(set([b"one", b"two"]), self.redis.sunion(["y", "z"])) 231 | eq_(set([b"one", b"two", b"three"]), self.redis.sunion("x", "y", "z")) 232 | eq_(set([b"one", b"two", b"three"]), self.redis.sunion(["x", "y"], "z")) 233 | 234 | def test_sunionstore(self): 235 | self.redis.sadd("x", "one", "two", "three") 236 | self.redis.sadd("y", "one") 237 | self.redis.sadd("z", "two") 238 | 239 | with assert_raises(Exception): 240 | self.redis.sunionstore("w", []) 241 | 242 | eq_(0, self.redis.sunionstore("w", "v")) 243 | eq_(set(), self.redis.smembers("w")) 244 | 245 | eq_(3, self.redis.sunionstore("w", "x")) 246 | eq_(set([b"one", b"two", b"three"]), self.redis.smembers("w")) 247 | 248 | eq_(1, self.redis.sunionstore("w", "v", "y")) 249 | eq_(set([b"one"]), self.redis.smembers("w")) 250 | 251 | eq_(2, self.redis.sunionstore("w", ["y", "z"])) 252 | eq_(set([b"one", b"two"]), self.redis.smembers("w")) 253 | 254 | eq_(3, self.redis.sunionstore("w", "x", "y", "z")) 255 | eq_(set([b"one", b"two", b"three"]), self.redis.smembers("w")) 256 | 257 | eq_(3, self.redis.sunionstore("w", ["x", "y"], "z")) 258 | eq_(set([b"one", b"two", b"three"]), self.redis.smembers("w")) 259 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /mockredis/tests/test_scan.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_ 2 | 3 | from mockredis.tests.fixtures import setup, teardown 4 | 5 | 6 | class TestRedisEmptyScans(object): 7 | """zero scan results tests""" 8 | 9 | def setup(self): 10 | setup(self) 11 | 12 | def teardown(self): 13 | teardown(self) 14 | 15 | def test_scans(self): 16 | def eq_scan(results, cursor, elements): 17 | """ 18 | Explicitly compare cursor and element by index as there 19 | redis-py currently returns a tuple for HSCAN and a list 20 | for the others, mockredis-py only returns lists, and it's 21 | not clear that emulating redis-py in this regard is "correct". 22 | """ 23 | eq_(results[0], cursor) 24 | eq_(results[1], elements) 25 | 26 | eq_scan(self.redis.scan(), 0, []) 27 | eq_scan(self.redis.sscan("foo"), 0, []) 28 | eq_scan(self.redis.zscan("foo"), 0, []) 29 | eq_scan(self.redis.hscan("foo"), 0, {}) 30 | 31 | keys = [] 32 | for k in self.redis.scan_iter(): 33 | keys.append(k) 34 | eq_(keys, []) 35 | 36 | members = [] 37 | for m in self.redis.sscan_iter('foo'): 38 | members.append(m) 39 | eq_(members, []) 40 | 41 | members = [] 42 | for m in self.redis.zscan_iter('foo'): 43 | members.append(m) 44 | eq_(members, []) 45 | 46 | members = [] 47 | for m in self.redis.hscan_iter('foo'): 48 | members.append(m) 49 | eq_(members, []) 50 | 51 | 52 | class TestRedisScan(object): 53 | """SCAN tests""" 54 | 55 | def setup(self): 56 | setup(self) 57 | self.redis.set('key_abc_1', '1') 58 | self.redis.set('key_abc_2', '2') 59 | self.redis.set('key_abc_3', '3') 60 | self.redis.set('key_abc_4', '4') 61 | self.redis.set('key_abc_5', '5') 62 | self.redis.set('key_abc_6', '6') 63 | 64 | self.redis.set('key_xyz_1', '1') 65 | self.redis.set('key_xyz_2', '2') 66 | self.redis.set('key_xyz_3', '3') 67 | self.redis.set('key_xyz_4', '4') 68 | self.redis.set('key_xyz_5', '5') 69 | 70 | def teardown(self): 71 | teardown(self) 72 | 73 | def test_scan(self): 74 | def do_full_scan(match, count): 75 | keys = set() # technically redis SCAN can return duplicate keys 76 | cursor = '0' 77 | result_cursor = None 78 | while result_cursor != 0: 79 | results = self.redis.scan(cursor=cursor, match=match, count=count) 80 | keys.update(results[1]) 81 | cursor = results[0] 82 | result_cursor = cursor 83 | return keys 84 | 85 | abc_keys = set([b'key_abc_1', b'key_abc_2', b'key_abc_3', b'key_abc_4', b'key_abc_5', b'key_abc_6']) # noqa 86 | eq_(do_full_scan('*abc*', 1), abc_keys) 87 | eq_(do_full_scan('*abc*', 2), abc_keys) 88 | eq_(do_full_scan('*abc*', 10), abc_keys) 89 | keys = set() 90 | for k in self.redis.scan_iter('*abc*'): 91 | keys.add(k) 92 | eq_(keys, abc_keys) 93 | 94 | xyz_keys = set([b'key_xyz_1', b'key_xyz_2', b'key_xyz_3', b'key_xyz_4', b'key_xyz_5']) 95 | eq_(do_full_scan('*xyz*', 1), xyz_keys) 96 | eq_(do_full_scan('*xyz*', 2), xyz_keys) 97 | eq_(do_full_scan('*xyz*', 10), xyz_keys) 98 | keys = set() 99 | for k in self.redis.scan_iter('*xyz*'): 100 | keys.add(k) 101 | eq_(keys, xyz_keys) 102 | 103 | one_keys = set([b'key_abc_1', b'key_xyz_1']) 104 | eq_(do_full_scan('*_1', 1), one_keys) 105 | eq_(do_full_scan('*_1', 2), one_keys) 106 | eq_(do_full_scan('*_1', 10), one_keys) 107 | keys = set() 108 | for k in self.redis.scan_iter('*_1'): 109 | keys.add(k) 110 | eq_(keys, one_keys) 111 | 112 | all_keys = abc_keys.union(xyz_keys) 113 | eq_(do_full_scan('*', 1), all_keys) 114 | eq_(do_full_scan('*', 2), all_keys) 115 | eq_(do_full_scan('*', 10), all_keys) 116 | keys = set() 117 | for k in self.redis.scan_iter('*'): 118 | keys.add(k) 119 | eq_(keys, all_keys) 120 | 121 | 122 | class TestRedisSScan(object): 123 | """SSCAN tests""" 124 | 125 | def setup(self): 126 | setup(self) 127 | self.redis.sadd('key', 'abc_1') 128 | self.redis.sadd('key', 'abc_2') 129 | self.redis.sadd('key', 'abc_3') 130 | self.redis.sadd('key', 'abc_4') 131 | self.redis.sadd('key', 'abc_5') 132 | self.redis.sadd('key', 'abc_6') 133 | 134 | self.redis.sadd('key', 'xyz_1') 135 | self.redis.sadd('key', 'xyz_2') 136 | self.redis.sadd('key', 'xyz_3') 137 | self.redis.sadd('key', 'xyz_4') 138 | self.redis.sadd('key', 'xyz_5') 139 | 140 | def teardown(self): 141 | teardown(self) 142 | 143 | def test_scan(self): 144 | def do_full_scan(name, match, count): 145 | keys = set() # technically redis SCAN can return duplicate keys 146 | cursor = '0' 147 | result_cursor = None 148 | while result_cursor != 0: 149 | results = self.redis.sscan(name, cursor=cursor, match=match, count=count) 150 | keys.update(results[1]) 151 | cursor = results[0] 152 | result_cursor = cursor 153 | return keys 154 | 155 | abc_members = set([b'abc_1', b'abc_2', b'abc_3', b'abc_4', b'abc_5', b'abc_6']) 156 | eq_(do_full_scan('key', '*abc*', 1), abc_members) 157 | eq_(do_full_scan('key', '*abc*', 2), abc_members) 158 | eq_(do_full_scan('key', '*abc*', 10), abc_members) 159 | members = set() 160 | for m in self.redis.sscan_iter('key', '*abc*'): 161 | members.add(m) 162 | eq_(members, abc_members) 163 | 164 | xyz_members = set([b'xyz_1', b'xyz_2', b'xyz_3', b'xyz_4', b'xyz_5']) 165 | eq_(do_full_scan('key', '*xyz*', 1), xyz_members) 166 | eq_(do_full_scan('key', '*xyz*', 2), xyz_members) 167 | eq_(do_full_scan('key', '*xyz*', 10), xyz_members) 168 | members = set() 169 | for m in self.redis.sscan_iter('key', '*xyz*'): 170 | members.add(m) 171 | eq_(members, xyz_members) 172 | 173 | one_members = set([b'abc_1', b'xyz_1']) 174 | eq_(do_full_scan('key', '*_1', 1), one_members) 175 | eq_(do_full_scan('key', '*_1', 2), one_members) 176 | eq_(do_full_scan('key', '*_1', 10), one_members) 177 | members = set() 178 | for m in self.redis.sscan_iter('key', '*_1'): 179 | members.add(m) 180 | eq_(members, one_members) 181 | 182 | all_members = abc_members.union(xyz_members) 183 | eq_(do_full_scan('key', '*', 1), all_members) 184 | eq_(do_full_scan('key', '*', 2), all_members) 185 | eq_(do_full_scan('key', '*', 10), all_members) 186 | members = set() 187 | for m in self.redis.sscan_iter('key', '*'): 188 | members.add(m) 189 | eq_(members, all_members) 190 | 191 | 192 | class TestRedisZScan(object): 193 | """ZSCAN tests""" 194 | 195 | def setup(self): 196 | setup(self) 197 | self.redis.zadd('key', 'abc_1', 1) 198 | self.redis.zadd('key', 'abc_2', 2) 199 | self.redis.zadd('key', 'abc_3', 3) 200 | self.redis.zadd('key', 'abc_4', 4) 201 | self.redis.zadd('key', 'abc_5', 5) 202 | self.redis.zadd('key', 'abc_6', 6) 203 | 204 | self.redis.zadd('key', 'xyz_1', 1) 205 | self.redis.zadd('key', 'xyz_2', 2) 206 | self.redis.zadd('key', 'xyz_3', 3) 207 | self.redis.zadd('key', 'xyz_4', 4) 208 | self.redis.zadd('key', 'xyz_5', 5) 209 | 210 | def teardown(self): 211 | teardown(self) 212 | 213 | def test_scan(self): 214 | def do_full_scan(name, match, count): 215 | keys = set() # technically redis SCAN can return duplicate keys 216 | cursor = '0' 217 | result_cursor = None 218 | while result_cursor != 0: 219 | results = self.redis.zscan(name, cursor=cursor, match=match, count=count) 220 | keys.update(results[1]) 221 | cursor = results[0] 222 | result_cursor = cursor 223 | return keys 224 | 225 | abc_members = set([(b'abc_1', 1), (b'abc_2', 2), (b'abc_3', 3), (b'abc_4', 4), (b'abc_5', 5), (b'abc_6', 6)]) # noqa 226 | eq_(do_full_scan('key', '*abc*', 1), abc_members) 227 | eq_(do_full_scan('key', '*abc*', 2), abc_members) 228 | eq_(do_full_scan('key', '*abc*', 10), abc_members) 229 | members = set() 230 | for m in self.redis.zscan_iter('key', '*abc*'): 231 | members.add(m) 232 | eq_(members, abc_members) 233 | 234 | xyz_members = set([(b'xyz_1', 1), (b'xyz_2', 2), (b'xyz_3', 3), (b'xyz_4', 4), (b'xyz_5', 5)]) # noqa 235 | eq_(do_full_scan('key', '*xyz*', 1), xyz_members) 236 | eq_(do_full_scan('key', '*xyz*', 2), xyz_members) 237 | eq_(do_full_scan('key', '*xyz*', 10), xyz_members) 238 | members = set() 239 | for m in self.redis.zscan_iter('key', '*xyz*'): 240 | members.add(m) 241 | eq_(members, xyz_members) 242 | 243 | one_members = set([(b'abc_1', 1), (b'xyz_1', 1)]) 244 | eq_(do_full_scan('key', '*_1', 1), one_members) 245 | eq_(do_full_scan('key', '*_1', 2), one_members) 246 | eq_(do_full_scan('key', '*_1', 10), one_members) 247 | members = set() 248 | for m in self.redis.zscan_iter('key', '*_1'): 249 | members.add(m) 250 | eq_(members, one_members) 251 | 252 | all_members = abc_members.union(xyz_members) 253 | eq_(do_full_scan('key', '*', 1), all_members) 254 | eq_(do_full_scan('key', '*', 2), all_members) 255 | eq_(do_full_scan('key', '*', 10), all_members) 256 | members = set() 257 | for m in self.redis.zscan_iter('key', '*'): 258 | members.add(m) 259 | eq_(members, all_members) 260 | 261 | 262 | class TestRedisHScan(object): 263 | """HSCAN tests""" 264 | 265 | def setup(self): 266 | setup(self) 267 | self.redis.hset('key', 'abc_1', 1) 268 | self.redis.hset('key', 'abc_2', 2) 269 | self.redis.hset('key', 'abc_3', 3) 270 | self.redis.hset('key', 'abc_4', 4) 271 | self.redis.hset('key', 'abc_5', 5) 272 | self.redis.hset('key', 'abc_6', 6) 273 | 274 | self.redis.hset('key', 'xyz_1', 1) 275 | self.redis.hset('key', 'xyz_2', 2) 276 | self.redis.hset('key', 'xyz_3', 3) 277 | self.redis.hset('key', 'xyz_4', 4) 278 | self.redis.hset('key', 'xyz_5', 5) 279 | 280 | def teardown(self): 281 | teardown(self) 282 | 283 | def test_scan(self): 284 | def do_full_scan(name, match, count): 285 | keys = {} 286 | cursor = '0' 287 | result_cursor = None 288 | while result_cursor != 0: 289 | results = self.redis.hscan(name, cursor=cursor, match=match, count=count) 290 | keys.update(results[1]) 291 | cursor = results[0] 292 | result_cursor = cursor 293 | return keys 294 | 295 | abc = {b'abc_1': b'1', b'abc_2': b'2', b'abc_3': b'3', b'abc_4': b'4', b'abc_5': b'5', b'abc_6': b'6'} # noqa 296 | eq_(do_full_scan('key', '*abc*', 1), abc) 297 | eq_(do_full_scan('key', '*abc*', 2), abc) 298 | eq_(do_full_scan('key', '*abc*', 10), abc) 299 | data = {} 300 | for k, v in self.redis.hscan_iter('key', '*abc*'): 301 | data[k] = v 302 | eq_(data, abc) 303 | 304 | xyz = {b'xyz_1': b'1', b'xyz_2': b'2', b'xyz_3': b'3', b'xyz_4': b'4', b'xyz_5': b'5'} 305 | eq_(do_full_scan('key', '*xyz*', 1), xyz) 306 | eq_(do_full_scan('key', '*xyz*', 2), xyz) 307 | eq_(do_full_scan('key', '*xyz*', 10), xyz) 308 | data = {} 309 | for k, v in self.redis.hscan_iter('key', '*xyz*'): 310 | data[k] = v 311 | eq_(data, xyz) 312 | 313 | all_1 = {b'abc_1': b'1', b'xyz_1': b'1'} 314 | eq_(do_full_scan('key', '*_1', 1), all_1) 315 | eq_(do_full_scan('key', '*_1', 2), all_1) 316 | eq_(do_full_scan('key', '*_1', 10), all_1) 317 | data = {} 318 | for k, v in self.redis.hscan_iter('key', '*_1'): 319 | data[k] = v 320 | eq_(data, all_1) 321 | 322 | abcxyz = abc 323 | abcxyz.update(xyz) 324 | eq_(do_full_scan('key', '*', 1), abcxyz) 325 | eq_(do_full_scan('key', '*', 2), abcxyz) 326 | eq_(do_full_scan('key', '*', 10), abcxyz) 327 | data = {} 328 | for k, v in self.redis.hscan_iter('key', '*'): 329 | data[k] = v 330 | eq_(data, abcxyz) 331 | -------------------------------------------------------------------------------- /mockredis/tests/test_string.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from nose.tools import eq_, ok_ 4 | 5 | from mockredis.client import get_total_milliseconds 6 | from mockredis.tests.fixtures import raises_response_error, setup, teardown 7 | 8 | 9 | class TestRedisString(object): 10 | """string tests""" 11 | 12 | def setup(self): 13 | setup(self) 14 | 15 | def teardown(self): 16 | teardown(self) 17 | 18 | def test_get(self): 19 | eq_(None, self.redis.get('key')) 20 | self.redis.set('key', 'value') 21 | eq_(b'value', self.redis.get('key')) 22 | 23 | def test_mget(self): 24 | eq_(None, self.redis.get('mget1')) 25 | eq_(None, self.redis.get('mget2')) 26 | eq_([None, None], self.redis.mget('mget1', 'mget2')) 27 | eq_([None, None], self.redis.mget(['mget1', 'mget2'])) 28 | 29 | self.redis.set('mget1', 'value1') 30 | self.redis.set('mget2', 'value2') 31 | eq_([b'value1', b'value2'], self.redis.mget('mget1', 'mget2')) 32 | eq_([b'value1', b'value2'], self.redis.mget(['mget1', 'mget2'])) 33 | 34 | def test_set_no_options(self): 35 | self.redis.set('key', 'value') 36 | eq_(b'value', self.redis.get('key')) 37 | 38 | def _assert_set_with_options(self, test_cases): 39 | """ 40 | Assert conditions for setting a key on the set function. 41 | 42 | The set function can take px, ex, nx and xx kwargs, this function asserts various 43 | conditions on the set depending on the combinations of kwargs: creation mode(nx,xx) 44 | and expiration(ex,px). 45 | 46 | E.g. verifying that a non-existent key does not get set if xx=True or gets set with nx=True 47 | iff it is absent. 48 | """ 49 | category, existing_key, cases = test_cases 50 | msg = "Failed in: {}".format(category) 51 | if existing_key: 52 | self.redis.set('key', 'value') 53 | for (key, value, expected_result), config in cases: 54 | # set with creation mode and expiry options 55 | result = self.redis.set(key, value, **config) 56 | eq_(expected_result, result, msg) 57 | if expected_result is not None: 58 | # if the set was expected to happen 59 | self._assert_was_set(key, value, config, msg) 60 | else: 61 | # if the set was not expected to happen 62 | self._assert_not_set(key, value, msg) 63 | 64 | def _assert_not_set(self, key, value, msg): 65 | """Check that the key and its timeout were not set""" 66 | 67 | # check that the value wasn't updated 68 | value = value.encode('utf8') if value is not None else value 69 | ok_(value != self.redis.get(key), msg) 70 | eq_(self.redis.ttl(key), None) 71 | 72 | def _assert_was_set(self, key, value, config, msg, delta=1): 73 | """Assert that the key was set along with timeout if applicable""" 74 | 75 | value = value.encode('utf8') if value is not None else value 76 | eq_(value, self.redis.get(key)) 77 | if "px" not in config and "ex" not in config: 78 | return 79 | # px should have been preferred over ex if it was specified 80 | ttl = self.redis.ttl(key) 81 | ok_(ttl > 0, msg) 82 | expected_ttl = int(config['px'] / 1000) if "px" in config else config["ex"] 83 | ok_(expected_ttl - ttl <= delta, msg) 84 | 85 | def test_set_with_options(self): 86 | """Test the set function with various combinations of arguments""" 87 | 88 | test_cases = [ 89 | ("1. px and ex are set, nx is always true & set on non-existing key", 90 | False, 91 | [(('key1', 'value1', None), dict(ex=20, px=70000, xx=True, nx=True)), 92 | (('key2', 'value1', True), dict(ex=20, px=70000, xx=False, nx=True)), 93 | (('key3', 'value2', True), dict(ex=20, px=70000, nx=True))]), 94 | 95 | ("2. px and ex are set, nx is always true & set on existing key", 96 | True, 97 | [(('key', 'value1', None), dict(ex=20, px=70000, xx=True, nx=True)), 98 | (('key', 'value1', None), dict(ex=20, px=7000, xx=False, nx=True)), 99 | (('key', 'value1', None), dict(ex=20, px=70000, nx=True))]), 100 | 101 | ("3. px and ex are set, xx is always true & set on existing key", 102 | True, 103 | [(('key', 'value1', None), dict(ex=20, px=70000, xx=True, nx=True)), 104 | (('key', 'value1', True), dict(ex=20, px=70000, xx=True, nx=False)), 105 | (('key', 'value4', True), dict(ex=20, px=70000, xx=True))]), 106 | 107 | ("4. px and ex are set, xx is always true & set on non-existing key", 108 | False, 109 | [(('key1', 'value1', None), dict(ex=20, px=70000, xx=True, nx=True)), 110 | (('key2', 'value2', None), dict(ex=20, px=70000, xx=True, nx=False)), 111 | (('key3', 'value3', None), dict(ex=20, px=70000, xx=True))]), 112 | 113 | ("5. either nx or xx defined and set to false or none defined" + 114 | " & set on existing key", 115 | True, 116 | [(('key', 'value1', True), dict(ex=20, px=70000, xx=False)), 117 | (('key', 'value2', True), dict(ex=20, px=70000, nx=False)), 118 | (('key', 'value3', True), dict(ex=20, px=70000))]), 119 | 120 | ("6. either nx or xx defined and set to false or none defined" + 121 | " & set on non-existing key", 122 | False, 123 | [(('key1', 'value1', True), dict(ex=20, px=70000, xx=False)), 124 | (('key2', 'value2', True), dict(ex=20, px=70000, nx=False)), 125 | (('key3', 'value3', True), dict(ex=20, px=70000))]), 126 | 127 | ("7: where neither px nor ex defined + set on existing key", 128 | True, 129 | [(('key', 'value2', None), dict(xx=True, nx=True)), 130 | (('key', 'value2', None), dict(xx=False, nx=True)), 131 | (('key', 'value2', True), dict(xx=True, nx=False)), 132 | (('key', 'value3', True), dict(xx=True)), 133 | (('key', 'value4', None), dict(nx=True)), 134 | (('key', 'value4', True), dict(xx=False)), 135 | (('key', 'value5', True), dict(nx=False))]), 136 | 137 | ("8: where neither px nor ex defined + set on non-existing key", 138 | False, 139 | [(('key1', 'value1', None), dict(xx=True, nx=True)), 140 | (('key2', 'value1', True), dict(xx=False, nx=True)), 141 | (('key3', 'value2', None), dict(xx=True, nx=False)), 142 | (('key4', 'value3', None), dict(xx=True)), 143 | (('key5', 'value4', True), dict(nx=True)), 144 | (('key6', 'value4', True), dict(xx=False)), 145 | (('key7', 'value5', True), dict(nx=False))]), 146 | 147 | ("9: where neither nx nor xx defined + set on existing key", 148 | True, 149 | [(('key', 'value1', True), dict(ex=20, px=70000)), 150 | (('key1', 'value12', True), dict(ex=20)), 151 | (('key1', 'value11', True), dict(px=20000))]), 152 | 153 | ("10: where neither nx nor xx is defined + set on non-existing key", 154 | False, 155 | [(('key1', 'value1', True), dict(ex=20, px=70000)), 156 | (('key2', 'value2', True), dict(ex=20)), 157 | (('key3', 'value3', True), dict(px=20000))])] 158 | 159 | for cases in test_cases: 160 | yield self._assert_set_with_options, cases 161 | 162 | def _assert_set_with_timeout(self, seconds): 163 | """ 164 | Assert both strict and non-strict that setex sets a key with a value along with a timeout. 165 | """ 166 | 167 | eq_(None, self.redis_strict.get('key')) 168 | eq_(None, self.redis.get('key')) 169 | 170 | ok_(self.redis_strict.setex('key', seconds, 'value')) 171 | ok_(self.redis.setex('key', 'value', seconds)) 172 | eq_(b'value', self.redis_strict.get('key')) 173 | eq_(b'value', self.redis.get('key')) 174 | 175 | ok_(self.redis_strict.ttl('key'), "expiration was not set correctly") 176 | ok_(self.redis.ttl('key'), "expiration was not set correctly") 177 | if isinstance(seconds, timedelta): 178 | seconds = seconds.seconds + seconds.days * 24 * 3600 179 | ok_(0 < self.redis_strict.ttl('key') <= seconds) 180 | ok_(0 < self.redis.ttl('key') <= seconds) 181 | 182 | def test_setex(self): 183 | test_cases = [20, timedelta(seconds=20)] 184 | for case in test_cases: 185 | yield self._assert_set_with_timeout, case 186 | 187 | @raises_response_error 188 | def test_setex_invalid_expiration(self): 189 | self.redis.setex('key', 'value', -2) 190 | 191 | @raises_response_error 192 | def test_strict_setex_invalid_expiration(self): 193 | self.redis_strict.setex('key', -2, 'value') 194 | 195 | @raises_response_error 196 | def test_setex_zero_expiration(self): 197 | self.redis.setex('key', 'value', 0) 198 | 199 | @raises_response_error 200 | def test_strict_setex_zero_expiration(self): 201 | self.redis_strict.setex('key', 0, 'value') 202 | 203 | def test_mset(self): 204 | ok_(self.redis.mset({"key1": "hello", "key2": ""})) 205 | ok_(self.redis.mset(**{"key3": "world", "key2": "there"})) 206 | eq_([b"hello", b"there", b"world"], self.redis.mget("key1", "key2", "key3")) 207 | 208 | def test_msetnx(self): 209 | ok_(self.redis.msetnx({"key1": "hello", "key2": "there"})) 210 | ok_(not self.redis.msetnx(**{"key3": "world", "key2": "there"})) 211 | eq_([b"hello", b"there", None], self.redis.mget("key1", "key2", "key3")) 212 | 213 | def test_psetex(self): 214 | test_cases = [200, timedelta(milliseconds=250)] 215 | for case in test_cases: 216 | yield self._assert_set_with_timeout_milliseconds, case 217 | 218 | @raises_response_error 219 | def test_psetex_invalid_expiration(self): 220 | self.redis.psetex('key', -20, 'value') 221 | 222 | @raises_response_error 223 | def test_psetex_zero_expiration(self): 224 | self.redis.psetex('key', 0, 'value') 225 | 226 | def _assert_set_with_timeout_milliseconds(self, milliseconds): 227 | """Assert that psetex sets a key with a value along with a timeout""" 228 | 229 | eq_(None, self.redis.get('key')) 230 | 231 | ok_(self.redis.psetex('key', milliseconds, 'value')) 232 | eq_(b'value', self.redis.get('key')) 233 | 234 | ok_(self.redis.pttl('key'), "expiration was not set correctly") 235 | if isinstance(milliseconds, timedelta): 236 | milliseconds = get_total_milliseconds(milliseconds) 237 | 238 | ok_(0 < self.redis.pttl('key') <= milliseconds) 239 | 240 | def test_setnx(self): 241 | """Check whether setnx sets a key iff it does not already exist""" 242 | 243 | ok_(self.redis.setnx('key', 'value')) 244 | ok_(not self.redis.setnx('key', 'different_value')) 245 | eq_(b'value', self.redis.get('key')) 246 | 247 | def test_delete(self): 248 | """Test if delete works""" 249 | 250 | test_cases = [('1', '1'), 251 | (('1', '2'), ('1', '2')), 252 | (('1', '2', '3'), ('1', '3')), 253 | (('1', '2'), '1'), 254 | ('1', '2')] 255 | for case in test_cases: 256 | yield self._assert_delete, case 257 | 258 | def test_delete_int(self): 259 | self.redis.set("1", "value") 260 | eq_(self.redis.delete(1), 1) 261 | 262 | def _assert_delete(self, data): 263 | """ 264 | Asserts that key(s) deletion along with removing timeouts if any, succeeds as expected 265 | """ 266 | to_create, to_delete = data 267 | for key in to_create: 268 | self.redis.set(key, "value", ex=200) 269 | 270 | eq_(self.redis.delete(*to_delete), len(set(to_create) & set(to_delete))) 271 | 272 | # verify if the keys that were to be deleted, were deleted along with the timeouts. 273 | for key in set(to_create) & set(to_delete): 274 | ok_(key not in self.redis) 275 | eq_(self.redis.ttl(key), None) 276 | 277 | # verify if the keys not to be deleted, were not deleted and their timeouts not removed. 278 | for key in set(to_create) - (set(to_create) & set(to_delete)): 279 | ok_(key in self.redis) 280 | ok_(self.redis.ttl(key) > 0) 281 | 282 | def test_getset(self): 283 | eq_(None, self.redis.get('getset_key')) 284 | eq_(None, self.redis.getset('getset_key', '1')) 285 | eq_(b'1', self.redis.getset('getset_key', '2')) 286 | eq_(b'2', self.redis.get('getset_key')) 287 | 288 | def test_setbit(self): 289 | 290 | # test behavior on empty keys 291 | eq_(0, self.redis.getbit("setbit_key", 0)) 292 | eq_(0, self.redis.getbit("setbit_key", 1024)) 293 | eq_(None, self.redis.get("setbit_key")) 294 | 295 | # test setting bits and getting bits 296 | for x in range(64): 297 | eq_(0, self.redis.setbit("setbit_key", x, 1)) 298 | eq_(1, self.redis.getbit("setbit_key", x)) 299 | eq_(1, self.redis.setbit("setbit_key", x, 0)) 300 | eq_(0, self.redis.getbit("setbit_key", x)) 301 | 302 | # test setting string and getting bits 303 | eq_(True, self.redis.set("setbit_key", b"\xaa\xaa")) 304 | for x in range(0, 16, 2): 305 | eq_(1, self.redis.getbit("setbit_key", x)) 306 | eq_(0, self.redis.getbit("setbit_key", x+1)) 307 | 308 | # test setting bits and getting string 309 | for x in range(16, 32, 2): 310 | eq_(0, self.redis.setbit("setbit_key", x, 1)) 311 | eq_(b"\xaa\xaa\xaa\xaa", self.redis.get("setbit_key")) 312 | -------------------------------------------------------------------------------- /mockredis/tests/test_script.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for scripts don't yet support verification against redis-server. 3 | """ 4 | from hashlib import sha1 5 | from unittest.case import SkipTest 6 | import sys 7 | import threading 8 | 9 | from nose.tools import assert_raises, eq_, ok_ 10 | 11 | from mockredis import MockRedis 12 | from mockredis.exceptions import RedisError 13 | from mockredis.script import Script as MockRedisScript 14 | from mockredis.tests.test_constants import ( 15 | LIST1, LIST2, 16 | SET1, 17 | VAL1, VAL2, VAL3, VAL4, 18 | LPOP_SCRIPT 19 | ) 20 | from mockredis.tests.fixtures import raises_response_error 21 | 22 | 23 | if sys.version_info >= (3, 0): 24 | long = int 25 | 26 | 27 | class TestScript(object): 28 | """ 29 | Tests for MockRedis scripting operations 30 | """ 31 | 32 | def setup(self): 33 | self.redis = MockRedis(load_lua_dependencies=False) 34 | self.LPOP_SCRIPT_SHA = sha1(LPOP_SCRIPT.encode("utf-8")).hexdigest() 35 | 36 | try: 37 | lua, lua_globals = MockRedisScript._import_lua(load_dependencies=False) 38 | except RuntimeError: 39 | raise SkipTest("mockredispy was not installed with lua support") 40 | 41 | self.lua = lua 42 | self.lua_globals = lua_globals 43 | 44 | assert_equal_list = """ 45 | function compare_list(list1, list2) 46 | if #list1 ~= #list2 then 47 | return false 48 | end 49 | for i, item1 in ipairs(list1) do 50 | if item1 ~= list2[i] then 51 | return false 52 | end 53 | end 54 | return true 55 | end 56 | 57 | function assert_equal_list(list1, list2) 58 | assert(compare_list(list1, list2)) 59 | end 60 | return assert_equal_list 61 | """ 62 | self.lua_assert_equal_list = self.lua.execute(assert_equal_list) 63 | 64 | assert_equal_list_with_pairs = """ 65 | function pair_exists(list1, key, value) 66 | i = 1 67 | for i, item1 in ipairs(list1) do 68 | if i%2 == 1 then 69 | if (list1[i] == key) and (list1[i + 1] == value) then 70 | return true 71 | end 72 | end 73 | end 74 | return false 75 | end 76 | 77 | function compare_list_with_pairs(list1, list2) 78 | if #list1 ~= #list2 or #list1 % 2 == 1 then 79 | return false 80 | end 81 | for i = 1, #list1, 2 do 82 | if not pair_exists(list2, list1[i], list1[i + 1]) then 83 | return false 84 | end 85 | end 86 | return true 87 | end 88 | 89 | function assert_equal_list_with_pairs(list1, list2) 90 | assert(compare_list_with_pairs(list1, list2)) 91 | end 92 | return assert_equal_list_with_pairs 93 | """ 94 | self.lua_assert_equal_list_with_pairs = self.lua.execute(assert_equal_list_with_pairs) 95 | 96 | compare_val = """ 97 | function compare_val(var1, var2) 98 | return var1 == var2 99 | end 100 | return compare_val 101 | """ 102 | self.lua_compare_val = self.lua.execute(compare_val) 103 | 104 | def test_register_script_lpush(self): 105 | # lpush two values 106 | script_content = "redis.call('LPUSH', KEYS[1], ARGV[1], ARGV[2])" 107 | script = self.redis.register_script(script_content) 108 | script(keys=[LIST1], args=[VAL1, VAL2]) 109 | 110 | # validate insertion 111 | eq_([VAL2, VAL1], self.redis.lrange(LIST1, 0, -1)) 112 | 113 | def test_register_script_lpop(self): 114 | self.redis.lpush(LIST1, VAL2, VAL1) 115 | 116 | # lpop one value 117 | script_content = "return redis.call('LPOP', KEYS[1])" 118 | script = self.redis.register_script(script_content) 119 | list_item = script(keys=[LIST1]) 120 | 121 | # validate lpop 122 | eq_(VAL1, list_item) 123 | eq_([VAL2], self.redis.lrange(LIST1, 0, -1)) 124 | 125 | def test_register_script_rpoplpush(self): 126 | self.redis.lpush(LIST1, VAL2, VAL1) 127 | self.redis.lpush(LIST2, VAL4, VAL3) 128 | 129 | # rpoplpush 130 | script_content = "redis.call('RPOPLPUSH', KEYS[1], KEYS[2])" 131 | script = self.redis.register_script(script_content) 132 | script(keys=[LIST1, LIST2]) 133 | 134 | # validate rpoplpush 135 | eq_([VAL1], self.redis.lrange(LIST1, 0, -1)) 136 | eq_([VAL2, VAL3, VAL4], self.redis.lrange(LIST2, 0, -1)) 137 | 138 | def test_register_script_rpop_lpush(self): 139 | self.redis.lpush(LIST1, VAL2, VAL1) 140 | self.redis.lpush(LIST2, VAL4, VAL3) 141 | 142 | # rpop from LIST1 and lpush the same value to LIST2 143 | script_content = """ 144 | local tmp_item = redis.call('RPOP', KEYS[1]) 145 | redis.call('LPUSH', KEYS[2], tmp_item) 146 | """ 147 | script = self.redis.register_script(script_content) 148 | script(keys=[LIST1, LIST2]) 149 | 150 | # validate rpop and then lpush 151 | eq_([VAL1], self.redis.lrange(LIST1, 0, -1)) 152 | eq_([VAL2, VAL3, VAL4], self.redis.lrange(LIST2, 0, -1)) 153 | 154 | def test_register_script_client(self): 155 | # lpush two values in LIST1 in first instance of redis 156 | self.redis.lpush(LIST1, VAL2, VAL1) 157 | 158 | # create script on first instance of redis 159 | script_content = LPOP_SCRIPT 160 | script = self.redis.register_script(script_content) 161 | 162 | # lpush two values in LIST1 in redis2 (second instance of redis) 163 | redis2 = MockRedis() 164 | redis2.lpush(LIST1, VAL4, VAL3) 165 | 166 | # execute LPOP script on redis2 instance 167 | list_item = script(keys=[LIST1], client=redis2) 168 | 169 | # validate lpop from LIST1 in redis2 170 | eq_(VAL3, list_item) 171 | eq_([VAL4], redis2.lrange(LIST1, 0, -1)) 172 | eq_([VAL1, VAL2], self.redis.lrange(LIST1, 0, -1)) 173 | 174 | def test_eval_lpush(self): 175 | # lpush two values 176 | script_content = "redis.call('LPUSH', KEYS[1], ARGV[1], ARGV[2])" 177 | self.redis.eval(script_content, 1, LIST1, VAL1, VAL2) 178 | 179 | # validate insertion 180 | eq_([VAL2, VAL1], self.redis.lrange(LIST1, 0, -1)) 181 | 182 | def test_eval_lpop(self): 183 | self.redis.lpush(LIST1, VAL2, VAL1) 184 | 185 | # lpop one value 186 | script_content = "return redis.call('LPOP', KEYS[1])" 187 | list_item = self.redis.eval(script_content, 1, LIST1) 188 | 189 | # validate lpop 190 | eq_(VAL1, list_item) 191 | eq_([VAL2], self.redis.lrange(LIST1, 0, -1)) 192 | 193 | def test_eval_lrem(self): 194 | self.redis.delete(LIST1) 195 | self.redis.lpush(LIST1, VAL1) 196 | 197 | # lrem one value 198 | script_content = "return redis.call('LREM', KEYS[1], 0, ARGV[1])" 199 | value = self.redis.eval(script_content, 1, LIST1, VAL1) 200 | 201 | eq_(value, 1) 202 | 203 | def test_eval_zadd(self): 204 | # The score and member are reversed when the client is not strict. 205 | self.redis.strict = False 206 | script_content = "return redis.call('zadd', KEYS[1], ARGV[1], ARGV[2])" 207 | self.redis.eval(script_content, 1, SET1, 42, VAL1) 208 | 209 | eq_(42, self.redis.zscore(SET1, VAL1)) 210 | 211 | def test_eval_zrangebyscore(self): 212 | # Make sure the limit is removed. 213 | script = "return redis.call('zrangebyscore',KEYS[1],ARGV[1],ARGV[2])" 214 | self.eval_zrangebyscore(script) 215 | 216 | def test_eval_zrangebyscore_with_limit(self): 217 | # Make sure the limit is removed. 218 | script = ("return redis.call('zrangebyscore', " 219 | "KEYS[1], ARGV[1], ARGV[2], 'LIMIT', 0, 2)") 220 | 221 | self.eval_zrangebyscore(script) 222 | 223 | def eval_zrangebyscore(self, script): 224 | self.redis.strict = False 225 | self.redis.zadd(SET1, VAL1, 1) 226 | self.redis.zadd(SET1, VAL2, 2) 227 | 228 | eq_([], self.redis.eval(script, 1, SET1, 0, 0)) 229 | eq_([VAL1], self.redis.eval(script, 1, SET1, 0, 1)) 230 | eq_([VAL1, VAL2], self.redis.eval(script, 1, SET1, 0, 2)) 231 | eq_([VAL2], self.redis.eval(script, 1, SET1, 2, 2)) 232 | 233 | def test_table_type(self): 234 | self.redis.lpush(LIST1, VAL2, VAL1) 235 | script_content = """ 236 | local items = redis.call('LRANGE', KEYS[1], ARGV[1], ARGV[2]) 237 | return type(items) 238 | """ 239 | script = self.redis.register_script(script_content) 240 | itemType = script(keys=[LIST1], args=[0, -1]) 241 | eq_('table', itemType) 242 | 243 | def test_script_hgetall(self): 244 | myhash = {"k1": "v1"} 245 | self.redis.hmset("myhash", myhash) 246 | script_content = """ 247 | return redis.call('HGETALL', KEYS[1]) 248 | """ 249 | script = self.redis.register_script(script_content) 250 | item = script(keys=["myhash"]) 251 | ok_(isinstance(item, list)) 252 | eq_(["k1", "v1"], item) 253 | 254 | def test_evalsha(self): 255 | self.redis.lpush(LIST1, VAL1) 256 | script = LPOP_SCRIPT 257 | sha = self.LPOP_SCRIPT_SHA 258 | 259 | # validator error when script not registered 260 | with assert_raises(RedisError) as redis_error: 261 | self.redis.evalsha(self.LPOP_SCRIPT_SHA, 1, LIST1) 262 | 263 | eq_("Sha not registered", str(redis_error.exception)) 264 | 265 | with assert_raises(RedisError): 266 | self.redis.evalsha(self.LPOP_SCRIPT_SHA, 1, LIST1) 267 | 268 | # load script and then evalsha 269 | eq_(sha, self.redis.script_load(script)) 270 | eq_(VAL1, self.redis.evalsha(sha, 1, LIST1)) 271 | eq_(0, self.redis.llen(LIST1)) 272 | 273 | def test_script_exists(self): 274 | script = LPOP_SCRIPT 275 | sha = self.LPOP_SCRIPT_SHA 276 | eq_([False], self.redis.script_exists(sha)) 277 | self.redis.register_script(script) 278 | eq_([True], self.redis.script_exists(sha)) 279 | 280 | def test_script_flush(self): 281 | script = LPOP_SCRIPT 282 | sha = self.LPOP_SCRIPT_SHA 283 | self.redis.register_script(script) 284 | eq_([True], self.redis.script_exists(sha)) 285 | self.redis.script_flush() 286 | eq_([False], self.redis.script_exists(sha)) 287 | 288 | def test_script_load(self): 289 | script = LPOP_SCRIPT 290 | sha = self.LPOP_SCRIPT_SHA 291 | eq_([False], self.redis.script_exists(sha)) 292 | eq_(sha, self.redis.script_load(script)) 293 | eq_([True], self.redis.script_exists(sha)) 294 | 295 | def test_lua_to_python_none(self): 296 | lval = self.lua.eval("") 297 | pval = MockRedisScript._lua_to_python(lval) 298 | ok_(pval is None) 299 | 300 | def test_lua_to_python_list(self): 301 | lval = self.lua.eval('{"val1", "val2"}') 302 | pval = MockRedisScript._lua_to_python(lval) 303 | ok_(isinstance(pval, list)) 304 | eq_(["val1", "val2"], pval) 305 | 306 | def test_lua_to_python_long(self): 307 | lval = self.lua.eval('22') 308 | pval = MockRedisScript._lua_to_python(lval) 309 | ok_(isinstance(pval, long)) 310 | eq_(22, pval) 311 | 312 | def test_lua_to_python_flota(self): 313 | lval = self.lua.eval('22.2') 314 | pval = MockRedisScript._lua_to_python(lval) 315 | ok_(isinstance(pval, float)) 316 | eq_(22.2, pval) 317 | 318 | def test_lua_to_python_string(self): 319 | lval = self.lua.eval('"somestring"') 320 | pval = MockRedisScript._lua_to_python(lval) 321 | ok_(isinstance(pval, str)) 322 | eq_("somestring", pval) 323 | 324 | def test_lua_to_python_bool(self): 325 | lval = self.lua.eval('true') 326 | pval = MockRedisScript._lua_to_python(lval) 327 | ok_(isinstance(pval, bool)) 328 | eq_(True, pval) 329 | 330 | def test_python_to_lua_none(self): 331 | pval = None 332 | lval = MockRedisScript._python_to_lua(pval) 333 | is_null = """ 334 | function is_null(var1) 335 | return var1 == nil 336 | end 337 | return is_null 338 | """ 339 | lua_is_null = self.lua.execute(is_null) 340 | ok_(MockRedisScript._lua_to_python(lua_is_null(lval))) 341 | 342 | def test_python_to_lua_string(self): 343 | pval = "somestring" 344 | lval = MockRedisScript._python_to_lua(pval) 345 | lval_expected = self.lua.eval('"somestring"') 346 | eq_("string", self.lua_globals.type(lval)) 347 | eq_(lval_expected, lval) 348 | 349 | def test_python_to_lua_list(self): 350 | pval = ["abc", "xyz"] 351 | lval = MockRedisScript._python_to_lua(pval) 352 | lval_expected = self.lua.eval('{"abc", "xyz"}') 353 | self.lua_assert_equal_list(lval_expected, lval) 354 | 355 | def test_python_to_lua_dict(self): 356 | pval = {"k1": "v1", "k2": "v2"} 357 | lval = MockRedisScript._python_to_lua(pval) 358 | lval_expected = self.lua.eval('{"k1", "v1", "k2", "v2"}') 359 | self.lua_assert_equal_list_with_pairs(lval_expected, lval) 360 | 361 | def test_python_to_lua_long(self): 362 | pval = long(10) 363 | lval = MockRedisScript._python_to_lua(pval) 364 | lval_expected = self.lua.eval('10') 365 | eq_("number", self.lua_globals.type(lval)) 366 | ok_(MockRedisScript._lua_to_python(self.lua_compare_val(lval_expected, lval))) 367 | 368 | def test_python_to_lua_float(self): 369 | pval = 10.1 370 | lval = MockRedisScript._python_to_lua(pval) 371 | lval_expected = self.lua.eval('10.1') 372 | eq_("number", self.lua_globals.type(lval)) 373 | ok_(MockRedisScript._lua_to_python(self.lua_compare_val(lval_expected, lval))) 374 | 375 | def test_python_to_lua_boolean(self): 376 | pval = True 377 | lval = MockRedisScript._python_to_lua(pval) 378 | eq_("boolean", self.lua_globals.type(lval)) 379 | ok_(MockRedisScript._lua_to_python(lval)) 380 | 381 | def test_lua_ok_return(self): 382 | script_content = "return {ok='OK'}" 383 | script = self.redis.register_script(script_content) 384 | eq_('OK', script()) 385 | 386 | @raises_response_error 387 | def test_lua_err_return(self): 388 | script_content = "return {err='ERROR Some message'}" 389 | script = self.redis.register_script(script_content) 390 | script() 391 | 392 | def test_concurrent_lua(self): 393 | script_content = """ 394 | local entry = redis.call('HGETALL', ARGV[1]) 395 | redis.call('HSET', ARGV[1], 'kk', 'vv') 396 | return entry 397 | """ 398 | script = self.redis.register_script(script_content) 399 | 400 | for i in range(500): 401 | self.redis.hmset(i, {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}) 402 | 403 | def lua_thread(): 404 | for i in range(500): 405 | script(args=[i]) 406 | 407 | active_threads = [] 408 | for i in range(10): 409 | thread = threading.Thread(target=lua_thread) 410 | active_threads.append(thread) 411 | thread.start() 412 | 413 | for thread in active_threads: 414 | thread.join() 415 | -------------------------------------------------------------------------------- /mockredis/tests/test_list.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import time 3 | 4 | from nose.tools import assert_less, assert_raises, eq_ 5 | 6 | from mockredis.tests.fixtures import setup, teardown 7 | from mockredis.tests.test_constants import ( 8 | LIST1, LIST2, VAL1, VAL2, VAL3, VAL4, 9 | bLIST1, bVAL1, bVAL2, bVAL3, bVAL4, 10 | ) 11 | 12 | 13 | @contextmanager 14 | def assert_elapsed_time(expected=1.0, delta=2.0): 15 | """ 16 | Validate that work encapsulated by this context manager 17 | takes at least and is within a delta of the expected amount of time. 18 | """ 19 | start = time.time() 20 | yield 21 | diff = time.time() - start 22 | assert_less(expected, diff) 23 | assert_less(diff, expected + delta) 24 | 25 | 26 | class TestRedisList(object): 27 | """list tests""" 28 | 29 | def setup(self): 30 | setup(self) 31 | 32 | def teardown(self): 33 | teardown(self) 34 | 35 | def test_initially_empty(self): 36 | """ 37 | List is created empty. 38 | """ 39 | eq_(0, len(self.redis.lrange(LIST1, 0, -1))) 40 | 41 | def test_llen(self): 42 | eq_(0, self.redis.llen(LIST1)) 43 | self.redis.lpush(LIST1, VAL1, VAL2) 44 | eq_(2, self.redis.llen(LIST1)) 45 | self.redis.lpop(LIST1) 46 | eq_(1, self.redis.llen(LIST1)) 47 | self.redis.lpop(LIST1) 48 | eq_(0, self.redis.llen(LIST1)) 49 | 50 | def test_lindex(self): 51 | eq_(None, self.redis.lindex(LIST1, 0)) 52 | eq_(False, self.redis.exists(LIST1)) 53 | self.redis.rpush(LIST1, VAL1, VAL2) 54 | eq_(bVAL1, self.redis.lindex(LIST1, 0)) 55 | eq_(bVAL2, self.redis.lindex(LIST1, 1)) 56 | eq_(None, self.redis.lindex(LIST1, 2)) 57 | eq_(bVAL2, self.redis.lindex(LIST1, -1)) 58 | eq_(bVAL1, self.redis.lindex(LIST1, -2)) 59 | eq_(None, self.redis.lindex(LIST1, -3)) 60 | self.redis.lpop(LIST1) 61 | eq_(bVAL2, self.redis.lindex(LIST1, 0)) 62 | eq_(None, self.redis.lindex(LIST1, 1)) 63 | 64 | def test_lpop(self): 65 | self.redis.rpush(LIST1, VAL1, VAL2) 66 | eq_(bVAL1, self.redis.lpop(LIST1)) 67 | eq_(1, len(self.redis.lrange(LIST1, 0, -1))) 68 | eq_(bVAL2, self.redis.lpop(LIST1)) 69 | eq_(0, len(self.redis.lrange(LIST1, 0, -1))) 70 | eq_(None, self.redis.lpop(LIST1)) 71 | eq_([], self.redis.keys("*")) 72 | 73 | def test_blpop(self): 74 | self.redis.rpush(LIST1, VAL1, VAL2) 75 | eq_((bLIST1, bVAL1), self.redis.blpop((LIST1, LIST2))) 76 | eq_(1, len(self.redis.lrange(LIST1, 0, -1))) 77 | eq_((bLIST1, bVAL2), self.redis.blpop(LIST1)) 78 | eq_(0, len(self.redis.lrange(LIST1, 0, -1))) 79 | 80 | timeout = 1 81 | with assert_elapsed_time(expected=timeout): 82 | eq_(None, self.redis.blpop(LIST1, timeout)) 83 | 84 | def test_lpush(self): 85 | """ 86 | Insertion maintains order but not uniqueness. 87 | """ 88 | # lpush two values 89 | eq_(1, self.redis.lpush(LIST1, VAL1)) 90 | eq_(2, self.redis.lpush(LIST1, VAL2)) 91 | 92 | # validate insertion 93 | eq_(b"list", self.redis.type(LIST1)) 94 | eq_([bVAL2, bVAL1], self.redis.lrange(LIST1, 0, -1)) 95 | 96 | # insert two more values with one repeated 97 | eq_(4, self.redis.lpush(LIST1, VAL1, VAL3)) 98 | 99 | # validate the update 100 | eq_(b"list", self.redis.type(LIST1)) 101 | eq_([bVAL3, bVAL1, bVAL2, bVAL1], 102 | self.redis.lrange(LIST1, 0, -1)) 103 | 104 | def test_rpop(self): 105 | self.redis.rpush(LIST1, VAL1, VAL2) 106 | eq_(bVAL2, self.redis.rpop(LIST1)) 107 | eq_(1, len(self.redis.lrange(LIST1, 0, -1))) 108 | eq_(bVAL1, self.redis.rpop(LIST1)) 109 | eq_(0, len(self.redis.lrange(LIST1, 0, -1))) 110 | eq_(None, self.redis.rpop(LIST1)) 111 | eq_([], self.redis.keys("*")) 112 | 113 | def test_brpop(self): 114 | self.redis.rpush(LIST1, VAL1, VAL2) 115 | eq_((bLIST1, bVAL2), self.redis.brpop((LIST2, LIST1))) 116 | eq_(1, len(self.redis.lrange(LIST1, 0, -1))) 117 | eq_((bLIST1, bVAL1), self.redis.brpop(LIST1)) 118 | eq_(0, len(self.redis.lrange(LIST1, 0, -1))) 119 | 120 | timeout = 1 121 | with assert_elapsed_time(expected=timeout): 122 | eq_(None, self.redis.brpop(LIST1, timeout)) 123 | 124 | eq_([], self.redis.keys("*")) 125 | 126 | def test_rpush(self): 127 | """ 128 | Insertion maintains order but not uniqueness. 129 | """ 130 | # rpush two values 131 | eq_(1, self.redis.rpush(LIST1, VAL1)) 132 | eq_(2, self.redis.rpush(LIST1, VAL2)) 133 | 134 | # validate insertion 135 | eq_(b"list", self.redis.type(LIST1)) 136 | eq_([bVAL1, bVAL2], self.redis.lrange(LIST1, 0, -1)) 137 | 138 | # insert two more values with one repeated 139 | eq_(4, self.redis.rpush(LIST1, VAL1, VAL3)) 140 | 141 | # validate the update 142 | eq_(b"list", self.redis.type(LIST1)) 143 | eq_([bVAL1, bVAL2, bVAL1, bVAL3], 144 | self.redis.lrange(LIST1, 0, -1)) 145 | 146 | def test_lrem(self): 147 | self.redis.rpush(LIST1, VAL1, VAL2, VAL1, VAL3, VAL4, VAL2) 148 | eq_(2, self.redis.lrem(LIST1, VAL1, 0)) 149 | eq_([bVAL2, bVAL3, bVAL4, bVAL2], 150 | self.redis.lrange(LIST1, 0, -1)) 151 | 152 | del self.redis[LIST1] 153 | self.redis.rpush(LIST1, VAL1, VAL2, VAL1, VAL3, VAL4, VAL2) 154 | eq_(1, self.redis.lrem(LIST1, VAL2, 1)) 155 | eq_([bVAL1, bVAL1, bVAL3, bVAL4, bVAL2], 156 | self.redis.lrange(LIST1, 0, -1)) 157 | 158 | del self.redis[LIST1] 159 | self.redis.rpush(LIST1, VAL1, VAL2, VAL1, VAL3, VAL4, VAL2) 160 | eq_(2, self.redis.lrem(LIST1, VAL1, 100)) 161 | eq_([bVAL2, bVAL3, bVAL4, bVAL2], 162 | self.redis.lrange(LIST1, 0, -1)) 163 | 164 | del self.redis[LIST1] 165 | self.redis.rpush(LIST1, VAL1, VAL2, VAL1, VAL3, VAL4, VAL2) 166 | eq_(1, self.redis.lrem(LIST1, VAL3, -1)) 167 | eq_([bVAL1, bVAL2, bVAL1, bVAL4, bVAL2], 168 | self.redis.lrange(LIST1, 0, -1)) 169 | 170 | del self.redis[LIST1] 171 | self.redis.rpush(LIST1, VAL1, VAL2, VAL1, VAL3, VAL4, VAL2) 172 | eq_(1, self.redis.lrem(LIST1, VAL2, -1)) 173 | eq_([bVAL1, bVAL2, bVAL1, bVAL3, bVAL4], 174 | self.redis.lrange(LIST1, 0, -1)) 175 | 176 | del self.redis[LIST1] 177 | self.redis.rpush(LIST1, VAL1, VAL2, VAL1, VAL3, VAL4, VAL2) 178 | eq_(2, self.redis.lrem(LIST1, VAL2, -2)) 179 | eq_([bVAL1, bVAL1, bVAL3, bVAL4], 180 | self.redis.lrange(LIST1, 0, -1)) 181 | 182 | # string conversion 183 | self.redis.rpush(1, 1, "2", 3) 184 | eq_(1, self.redis.lrem(1, "1")) 185 | eq_(1, self.redis.lrem("1", 2)) 186 | eq_([b"3"], self.redis.lrange(1, 0, -1)) 187 | del self.redis["1"] 188 | 189 | del self.redis[LIST1] 190 | self.redis.rpush(LIST1, VAL1) 191 | eq_(1, self.redis.lrem(LIST1, VAL1)) 192 | eq_([], self.redis.lrange(LIST1, 0, -1)) 193 | eq_([], self.redis.keys("*")) 194 | 195 | eq_(0, self.redis.lrem("NON_EXISTENT_LIST", VAL1, 0)) 196 | 197 | def test_brpoplpush(self): 198 | self.redis.rpush(LIST1, VAL1, VAL2) 199 | self.redis.rpush(LIST2, VAL3, VAL4) 200 | transfer_item = self.redis.brpoplpush(LIST1, LIST2) 201 | eq_(bVAL2, transfer_item) 202 | eq_([bVAL1], self.redis.lrange(LIST1, 0, -1)) 203 | eq_([bVAL2, bVAL3, bVAL4], 204 | self.redis.lrange(LIST2, 0, -1)) 205 | transfer_item = self.redis.brpoplpush(LIST1, LIST2) 206 | eq_(bVAL1, transfer_item) 207 | eq_([], self.redis.lrange(LIST1, 0, -1)) 208 | eq_([bVAL1, bVAL2, bVAL3, bVAL4], 209 | self.redis.lrange(LIST2, 0, -1)) 210 | 211 | timeout = 1 212 | with assert_elapsed_time(expected=timeout): 213 | eq_(None, self.redis.brpoplpush(LIST1, LIST2, timeout)) 214 | 215 | def test_rpoplpush(self): 216 | self.redis.rpush(LIST1, VAL1, VAL2) 217 | self.redis.rpush(LIST2, VAL3, VAL4) 218 | transfer_item = self.redis.rpoplpush(LIST1, LIST2) 219 | eq_(bVAL2, transfer_item) 220 | eq_([bVAL1], self.redis.lrange(LIST1, 0, -1)) 221 | eq_([bVAL2, bVAL3, bVAL4], self.redis.lrange(LIST2, 0, -1)) 222 | 223 | def test_rpoplpush_with_empty_source(self): 224 | # source list is empty 225 | del self.redis[LIST1] 226 | self.redis.rpush(LIST2, VAL3, VAL4) 227 | transfer_item = self.redis.rpoplpush(LIST1, LIST2) 228 | eq_(None, transfer_item) 229 | eq_([], self.redis.lrange(LIST1, 0, -1)) 230 | # nothing has been added to the destination queue 231 | eq_([bVAL3, bVAL4], self.redis.lrange(LIST2, 0, -1)) 232 | 233 | def test_rpoplpush_source_with_empty_string(self): 234 | # source list contains empty string 235 | self.redis.rpush(LIST1, '') 236 | self.redis.rpush(LIST2, VAL3, VAL4) 237 | eq_(1, self.redis.llen(LIST1)) 238 | eq_(2, self.redis.llen(LIST2)) 239 | 240 | transfer_item = self.redis.rpoplpush(LIST1, LIST2) 241 | eq_(b'', transfer_item) 242 | eq_(0, self.redis.llen(LIST1)) 243 | eq_(3, self.redis.llen(LIST2)) 244 | eq_([], self.redis.lrange(LIST1, 0, -1)) 245 | # empty string is added to the destination queue 246 | eq_([b'', bVAL3, bVAL4], self.redis.lrange(LIST2, 0, -1)) 247 | 248 | def test_lrange_get_all(self): 249 | """Cases for returning entire list""" 250 | values = [bVAL4, bVAL3, bVAL2, bVAL1] 251 | 252 | eq_([], self.redis.lrange(LIST1, 0, 6)) 253 | eq_([], self.redis.lrange(LIST1, 0, -1)) 254 | self.redis.lpush(LIST1, *reversed(values)) 255 | 256 | # Check with exact range 257 | eq_(values, self.redis.lrange(LIST1, 0, 3)) 258 | # Check with negative index 259 | eq_(values, self.redis.lrange(LIST1, 0, -1)) 260 | # Check with range larger than length of list 261 | eq_(values, self.redis.lrange(LIST1, 0, 6)) 262 | 263 | def test_lrange_get_sublist(self): 264 | """Cases for returning partial list""" 265 | values = [bVAL4, bVAL3, bVAL2, bVAL1] 266 | 267 | eq_([], self.redis.lrange(LIST1, 0, 6)) 268 | eq_([], self.redis.lrange(LIST1, 0, -1)) 269 | self.redis.lpush(LIST1, *reversed(values)) 270 | 271 | # Check from left end of the list 272 | eq_(values[:2], self.redis.lrange(LIST1, 0, 1)) 273 | # Check from right end of the list 274 | eq_(values[2:4], self.redis.lrange(LIST1, 2, 3)) 275 | # Check from right end of the list with negative range 276 | eq_(values[-2:], self.redis.lrange(LIST1, -2, -1)) 277 | # Check from middle of the list 278 | eq_(values[1:3], self.redis.lrange(LIST1, 1, 2)) 279 | 280 | def test_ltrim_retain_all(self): 281 | values = [bVAL4, bVAL3, bVAL2, bVAL1] 282 | self._reinitialize_list(LIST1, *values) 283 | 284 | self.redis.ltrim(LIST1, 0, -1) 285 | eq_(values, self.redis.lrange(LIST1, 0, -1)) 286 | 287 | self.redis.ltrim(LIST1, 0, len(values) - 1) 288 | eq_(values, self.redis.lrange(LIST1, 0, -1)) 289 | 290 | self.redis.ltrim(LIST1, 0, len(values) + 1) 291 | eq_(values, self.redis.lrange(LIST1, 0, -1)) 292 | 293 | self.redis.ltrim(LIST1, -1 * len(values), -1) 294 | eq_(values, self.redis.lrange(LIST1, 0, -1)) 295 | 296 | self.redis.ltrim(LIST1, -1 * (len(values) + 1), -1) 297 | eq_(values, self.redis.lrange(LIST1, 0, -1)) 298 | 299 | def test_ltrim_remove_all(self): 300 | values = [bVAL4, bVAL3, bVAL2, bVAL1] 301 | self._reinitialize_list(LIST1, *values) 302 | 303 | self.redis.ltrim(LIST1, 2, 1) 304 | eq_([], self.redis.lrange(LIST1, 0, -1)) 305 | 306 | self._reinitialize_list(LIST1, *values) 307 | self.redis.ltrim(LIST1, -1, -2) 308 | eq_([], self.redis.lrange(LIST1, 0, -1)) 309 | 310 | self._reinitialize_list(LIST1, *values) 311 | self.redis.ltrim(LIST1, 2, -3) 312 | eq_([], self.redis.lrange(LIST1, 0, -1)) 313 | 314 | self._reinitialize_list(LIST1, *values) 315 | self.redis.ltrim(LIST1, -1, 2) 316 | eq_([], self.redis.lrange(LIST1, 0, -1)) 317 | 318 | def test_ltrim(self): 319 | values = [bVAL4, bVAL3, bVAL2, bVAL1] 320 | self._reinitialize_list(LIST1, *values) 321 | 322 | self.redis.ltrim(LIST1, 1, 2) 323 | eq_(values[1:3], self.redis.lrange(LIST1, 0, -1)) 324 | 325 | self._reinitialize_list(LIST1, *values) 326 | self.redis.ltrim(LIST1, -3, -1) 327 | eq_(values[-3:], self.redis.lrange(LIST1, 0, -1)) 328 | 329 | self._reinitialize_list(LIST1, *values) 330 | self.redis.ltrim(LIST1, 1, 5) 331 | eq_(values[1:5], self.redis.lrange(LIST1, 0, -1)) 332 | 333 | self._reinitialize_list(LIST1, *values) 334 | self.redis.ltrim(LIST1, -100, 2) 335 | eq_(values[-100:3], self.redis.lrange(LIST1, 0, -1)) 336 | 337 | def test_sort(self): 338 | values = [b'0.1', b'2', b'1.3'] 339 | self._reinitialize_list(LIST1, *values) 340 | 341 | # test unsorted 342 | eq_(self.redis.sort(LIST1, by='nosort'), values) 343 | 344 | # test straightforward sort 345 | eq_(self.redis.sort(LIST1), [b'0.1', b'1.3', b'2']) 346 | 347 | # test alpha vs numeric sort 348 | values = [-1, -2] 349 | self._reinitialize_list(LIST1, *values) 350 | eq_(self.redis.sort(LIST1, alpha=True), [b'-1', b'-2']) 351 | eq_(self.redis.sort(LIST1, alpha=False), [b'-2', b'-1']) 352 | 353 | values = ['0.1', '2', '1.3'] 354 | self._reinitialize_list(LIST1, *values) 355 | 356 | # test returning values sorted by values of other keys 357 | self.redis.set('by_0.1', '3') 358 | self.redis.set('by_2', '2') 359 | self.redis.set('by_1.3', '1') 360 | eq_(self.redis.sort(LIST1, by='by_*'), [b'1.3', b'2', b'0.1']) 361 | 362 | # test returning values from other keys sorted by list 363 | self.redis.set('get1_0.1', 'a') 364 | self.redis.set('get1_2', 'b') 365 | self.redis.set('get1_1.3', 'c') 366 | eq_(self.redis.sort(LIST1, get='get1_*'), [b'a', b'c', b'b']) 367 | 368 | # test storing result 369 | eq_(self.redis.sort(LIST1, get='get1_*', store='result'), 3) 370 | eq_(self.redis.llen('result'), 3) 371 | eq_(self.redis.lrange('result', 0, -1), [b'a', b'c', b'b']) 372 | 373 | # test desc (reverse order) 374 | eq_(self.redis.sort(LIST1, get='get1_*', desc=True), [b'b', b'c', b'a']) 375 | 376 | # test multiple gets without grouping 377 | self.redis.set('get2_0.1', 'x') 378 | self.redis.set('get2_2', 'y') 379 | self.redis.set('get2_1.3', 'z') 380 | eq_(self.redis.sort(LIST1, get=['get1_*', 'get2_*']), [b'a', b'x', b'c', b'z', b'b', b'y']) 381 | 382 | # test start and num apply to sorted items not final flat list of values 383 | eq_(self.redis.sort(LIST1, get=['get1_*', 'get2_*'], start=1, num=1), [b'c', b'z']) 384 | 385 | # test multiple gets with grouping 386 | eq_(self.redis.sort(LIST1, get=['get1_*', 'get2_*'], groups=True), [(b'a', b'x'), (b'c', b'z'), (b'b', b'y')]) # noqa 387 | 388 | # test start and num 389 | eq_(self.redis.sort(LIST1, get=['get1_*', 'get2_*'], groups=True, start=1, num=1), [(b'c', b'z')]) # noqa 390 | eq_(self.redis.sort(LIST1, get=['get1_*', 'get2_*'], groups=True, start=1, num=2), [(b'c', b'z'), (b'b', b'y')]) # noqa 391 | 392 | def test_lset(self): 393 | with assert_raises(Exception): 394 | self.redis.lset(LIST1, 1, VAL1) 395 | 396 | self.redis.lpush(LIST1, VAL2) 397 | eq_([bVAL2], self.redis.lrange(LIST1, 0, -1)) 398 | 399 | with assert_raises(Exception): 400 | self.redis.lset(LIST1, 1, VAL1) 401 | 402 | self.redis.lset(LIST1, 0, VAL1) 403 | eq_([bVAL1], self.redis.lrange(LIST1, 0, -1)) 404 | 405 | def test_push_pop_returns_str(self): 406 | key = 'l' 407 | values = ['5', 5, [], {}] 408 | for v in values: 409 | self.redis.rpush(key, v) 410 | eq_(self.redis.lpop(key), str(v).encode('utf8')) 411 | 412 | def _reinitialize_list(self, key, *values): 413 | """ 414 | Re-initialize the list 415 | """ 416 | self.redis.delete(LIST1) 417 | self.redis.lpush(LIST1, *reversed(values)) 418 | -------------------------------------------------------------------------------- /mockredis/tests/test_zset.py: -------------------------------------------------------------------------------- 1 | from nose.tools import assert_raises, eq_, ok_ 2 | 3 | from mockredis.tests.fixtures import setup, teardown 4 | 5 | 6 | class TestRedisZset(object): 7 | """zset tests""" 8 | 9 | def setup(self): 10 | setup(self) 11 | 12 | def teardown(self): 13 | teardown(self) 14 | 15 | def test_zadd(self): 16 | key = "zset" 17 | values = [("one", 1), ("uno", 1), ("two", 2), ("three", 3)] 18 | for member, score in values: 19 | eq_(1, self.redis.zadd(key, member, score)) 20 | 21 | def test_zadd_strict(self): 22 | """Argument order for zadd depends on strictness""" 23 | key = "zset" 24 | values = [("one", 1), ("uno", 1), ("two", 2), ("three", 3)] 25 | for member, score in values: 26 | eq_(1, self.redis_strict.zadd(key, score, member)) 27 | 28 | def test_zadd_duplicate_key(self): 29 | key = "zset" 30 | eq_(1, self.redis.zadd(key, "one", 1.0)) 31 | eq_(0, self.redis.zadd(key, "one", 2.0)) 32 | 33 | def test_zadd_wrong_type(self): 34 | key = "zset" 35 | self.redis.set(key, "value") 36 | with assert_raises(Exception): 37 | self.redis.zadd(key, "one", 2.0) 38 | 39 | def test_zadd_multiple_bad_args(self): 40 | key = "zset" 41 | args = ["one", 1, "two"] 42 | with assert_raises(Exception): 43 | self.redis.zadd(key, *args) 44 | 45 | def test_zadd_multiple_bad_score(self): 46 | key = "zset" 47 | with assert_raises(Exception): 48 | self.redis.zadd(key, "one", "two") 49 | 50 | def test_zadd_multiple_args(self): 51 | key = "zset" 52 | args = ["one", 1, "uno", 1, "two", 2, "three", 3] 53 | eq_(4, self.redis.zadd(key, *args)) 54 | 55 | def test_zadd_multiple_kwargs(self): 56 | key = "zset" 57 | kwargs = {"one": 1, "uno": 1, "two": 2, "three": 3} 58 | eq_(4, self.redis.zadd(key, **kwargs)) 59 | 60 | def test_zcard(self): 61 | key = "zset" 62 | eq_(0, self.redis.zcard(key)) 63 | self.redis.zadd(key, "one", 1) 64 | eq_(1, self.redis.zcard(key)) 65 | self.redis.zadd(key, "one", 2) 66 | eq_(1, self.redis.zcard(key)) 67 | self.redis.zadd(key, "two", 2) 68 | eq_(2, self.redis.zcard(key)) 69 | 70 | def test_zincrby(self): 71 | key = "zset" 72 | eq_(1.0, self.redis.zincrby(key, "member1")) 73 | eq_(2.0, self.redis.zincrby(key, "member2", 2)) 74 | eq_(-1.0, self.redis.zincrby(key, "member1", -2)) 75 | 76 | def test_zrange(self): 77 | key = "zset" 78 | eq_([], self.redis.zrange(key, 0, -1)) 79 | self.redis.zadd(key, "one", 1.5) 80 | self.redis.zadd(key, "two", 2.5) 81 | self.redis.zadd(key, "three", 3.5) 82 | 83 | # full range 84 | eq_([b"one", b"two", b"three"], 85 | self.redis.zrange(key, 0, -1)) 86 | # withscores 87 | eq_([(b"one", 1.5), (b"two", 2.5), (b"three", 3.5)], 88 | self.redis.zrange(key, 0, -1, withscores=True)) 89 | 90 | with assert_raises(ValueError): 91 | # invalid literal for int() with base 10 92 | self.redis.zrange(key, 0, -1, withscores=True, score_cast_func=int) 93 | 94 | # score_cast_func 95 | def cast_to_int(score): 96 | return int(float(score)) 97 | 98 | eq_([(b"one", 1), (b"two", 2), (b"three", 3)], 99 | self.redis.zrange(key, 0, -1, withscores=True, score_cast_func=cast_to_int)) 100 | 101 | # positive ranges 102 | eq_([b"one"], self.redis.zrange(key, 0, 0)) 103 | eq_([b"one", b"two"], self.redis.zrange(key, 0, 1)) 104 | eq_([b"one", b"two", b"three"], self.redis.zrange(key, 0, 2)) 105 | eq_([b"one", b"two", b"three"], self.redis.zrange(key, 0, 3)) 106 | eq_([b"two", b"three"], self.redis.zrange(key, 1, 2)) 107 | eq_([b"three"], self.redis.zrange(key, 2, 3)) 108 | 109 | # negative ends 110 | eq_([b"one", b"two", b"three"], self.redis.zrange(key, 0, -1)) 111 | eq_([b"one", b"two"], self.redis.zrange(key, 0, -2)) 112 | eq_([b"one"], self.redis.zrange(key, 0, -3)) 113 | eq_([], self.redis.zrange(key, 0, -4)) 114 | 115 | # negative starts 116 | eq_([], self.redis.zrange(key, -1, 0)) 117 | eq_([b"three"], self.redis.zrange(key, -1, -1)) 118 | eq_([b"two", b"three"], self.redis.zrange(key, -2, -1)) 119 | eq_([b"one", b"two", b"three"], self.redis.zrange(key, -3, -1)) 120 | eq_([b"one", b"two", b"three"], self.redis.zrange(key, -4, -1)) 121 | 122 | # desc 123 | eq_([b"three", b"two", b"one"], self.redis.zrange(key, 0, 2, desc=True)) 124 | eq_([b"two", b"one"], self.redis.zrange(key, 1, 2, desc=True)) 125 | eq_([b"three", b"two"], self.redis.zrange(key, 0, 1, desc=True)) 126 | 127 | def test_zrem(self): 128 | key = "zset" 129 | ok_(not self.redis.zrem(key, "two")) 130 | 131 | self.redis.zadd(key, "one", 1.0) 132 | eq_(1, self.redis.zcard(key)) 133 | eq_([b"zset"], self.redis.keys("*")) 134 | 135 | ok_(self.redis.zrem(key, "one")) 136 | eq_(0, self.redis.zcard(key)) 137 | eq_([], self.redis.keys("*")) 138 | 139 | def test_zscore(self): 140 | key = "zset" 141 | eq_(None, self.redis.zscore(key, "one")) 142 | 143 | self.redis.zadd(key, "one", 1.0) 144 | eq_(1.0, self.redis.zscore(key, "one")) 145 | 146 | def test_zscore_int_member(self): 147 | key = "zset" 148 | eq_(None, self.redis.zscore(key, 1)) 149 | 150 | self.redis.zadd(key, 1, 1.0) 151 | eq_(1.0, self.redis.zscore(key, 1)) 152 | 153 | def test_zrank(self): 154 | key = "zset" 155 | eq_(None, self.redis.zrank(key, "two")) 156 | 157 | self.redis.zadd(key, "one", 1.0) 158 | self.redis.zadd(key, "two", 2.0) 159 | eq_(0, self.redis.zrank(key, "one")) 160 | eq_(1, self.redis.zrank(key, "two")) 161 | 162 | def test_zrank_int_member(self): 163 | key = "zset" 164 | eq_(None, self.redis.zrank(key, 2)) 165 | 166 | self.redis.zadd(key, "one", 1.0) 167 | self.redis.zadd(key, 2, 2.0) 168 | eq_(0, self.redis.zrank(key, "one")) 169 | eq_(1, self.redis.zrank(key, 2)) 170 | 171 | def test_zcount(self): 172 | key = "zset" 173 | eq_(0, self.redis.zcount(key, "-inf", "inf")) 174 | 175 | self.redis.zadd(key, "one", 1.0) 176 | self.redis.zadd(key, "two", 2.0) 177 | 178 | eq_(2, self.redis.zcount(key, "-inf", "inf")) 179 | eq_(1, self.redis.zcount(key, "-inf", 1.0)) 180 | eq_(1, self.redis.zcount(key, "-inf", 1.5)) 181 | eq_(2, self.redis.zcount(key, "-inf", 2.0)) 182 | eq_(2, self.redis.zcount(key, "-inf", 2.5)) 183 | eq_(1, self.redis.zcount(key, 0.5, 1.0)) 184 | eq_(1, self.redis.zcount(key, 0.5, 1.5)) 185 | eq_(2, self.redis.zcount(key, 0.5, 2.0)) 186 | eq_(2, self.redis.zcount(key, 0.5, 2.5)) 187 | eq_(2, self.redis.zcount(key, 0.5, "inf")) 188 | 189 | eq_(0, self.redis.zcount(key, "inf", "-inf")) 190 | eq_(0, self.redis.zcount(key, 2.0, 0.5)) 191 | 192 | def test_zrangebyscore(self): 193 | key = "zset" 194 | eq_([], self.redis.zrangebyscore(key, "-inf", "inf")) 195 | self.redis.zadd(key, "one", 1.5) 196 | self.redis.zadd(key, "two", 2.5) 197 | self.redis.zadd(key, "three", 3.5) 198 | 199 | eq_([b"one", b"two", b"three"], 200 | self.redis.zrangebyscore(key, "-inf", "inf")) 201 | eq_([(b"one", 1.5), (b"two", 2.5), (b"three", 3.5)], 202 | self.redis.zrangebyscore(key, "-inf", "inf", withscores=True)) 203 | 204 | with assert_raises(ValueError): 205 | # invalid literal for int() with base 10 206 | self.redis.zrangebyscore(key, 207 | "-inf", 208 | "inf", 209 | withscores=True, 210 | score_cast_func=int) 211 | 212 | def cast_score(score): 213 | return int(float(score)) 214 | 215 | eq_([(b"one", 1), (b"two", 2), (b"three", 3)], 216 | self.redis.zrangebyscore(key, 217 | "-inf", 218 | "inf", 219 | withscores=True, 220 | score_cast_func=cast_score)) 221 | 222 | eq_([b"one"], 223 | self.redis.zrangebyscore(key, 1.0, 2.0)) 224 | eq_([b"one", b"two"], 225 | self.redis.zrangebyscore(key, 1.0, 3.0)) 226 | eq_([b"one"], 227 | self.redis.zrangebyscore(key, 1.0, 3.0, start=0, num=1)) 228 | eq_([b"two"], 229 | self.redis.zrangebyscore(key, 1.0, 3.0, start=1, num=1)) 230 | eq_([b"two", b"three"], 231 | self.redis.zrangebyscore(key, 1.0, 3.5, start=1, num=4)) 232 | eq_([], 233 | self.redis.zrangebyscore(key, 1.0, 3.5, start=3, num=4)) 234 | 235 | def test_zrangebyscore_inclusive(self): 236 | key = "zset" 237 | eq_([], self.redis.zrangebyscore(key, "-inf", "inf")) 238 | self.redis.zadd(key, "one", 1.5) 239 | self.redis.zadd(key, "two", 2.5) 240 | self.redis.zadd(key, "three", 3.5) 241 | self.redis.zadd(key, "four", 4.5) 242 | 243 | eq_([b"one"], 244 | self.redis.zrangebyscore(key, 1.0, '(2.5')) 245 | eq_([b"two", b"three"], 246 | self.redis.zrangebyscore(key, '(1.5', 3.5)) 247 | eq_([b"one"], 248 | self.redis.zrangebyscore(key, 1.0, '(3.5', start=0, num=1)) 249 | eq_([b"three"], 250 | self.redis.zrangebyscore(key, '(1.5', 3.5, start=1, num=1)) 251 | eq_([b"two", b"three"], 252 | self.redis.zrangebyscore(key, 1.0, '(4.5', start=1, num=4)) 253 | eq_([], 254 | self.redis.zrangebyscore(key, 1.0, '(4.5', start=3, num=4)) 255 | 256 | def test_zrevrank(self): 257 | key = "zset" 258 | eq_(None, self.redis.zrevrank(key, "two")) 259 | 260 | self.redis.zadd(key, "one", 1.0) 261 | self.redis.zadd(key, "two", 2.0) 262 | eq_(1, self.redis.zrevrank(key, "one")) 263 | eq_(0, self.redis.zrevrank(key, "two")) 264 | 265 | def test_zrevrangebyscore(self): 266 | key = "zset" 267 | eq_([], self.redis.zrevrangebyscore(key, "inf", "-inf")) 268 | self.redis.zadd(key, "one", 1.5) 269 | self.redis.zadd(key, "two", 2.5) 270 | self.redis.zadd(key, "three", 3.5) 271 | 272 | eq_([b"three", b"two", b"one"], 273 | self.redis.zrevrangebyscore(key, "inf", "-inf")) 274 | eq_([(b"three", 3.5), (b"two", 2.5), (b"one", 1.5)], 275 | self.redis.zrevrangebyscore(key, "inf", "-inf", withscores=True)) 276 | 277 | with assert_raises(ValueError): 278 | # invalid literal for int() with base 10 279 | self.redis.zrevrangebyscore(key, 280 | "inf", 281 | "-inf", 282 | withscores=True, 283 | score_cast_func=int) 284 | 285 | def cast_score(score): 286 | return int(float(score)) 287 | 288 | eq_([(b"three", 3), (b"two", 2), (b"one", 1)], 289 | self.redis.zrevrangebyscore(key, 290 | "inf", 291 | "-inf", 292 | withscores=True, 293 | score_cast_func=cast_score)) 294 | 295 | eq_([b"one"], 296 | self.redis.zrevrangebyscore(key, 2.0, 1.0)) 297 | eq_([b"two", b"one"], 298 | self.redis.zrevrangebyscore(key, 3.0, 1.0)) 299 | eq_([b"two"], 300 | self.redis.zrevrangebyscore(key, 3.0, 1.0, start=0, num=1)) 301 | eq_([b"one"], 302 | self.redis.zrevrangebyscore(key, 3.0, 1.0, start=1, num=1)) 303 | eq_([b"two", b"one"], 304 | self.redis.zrevrangebyscore(key, 3.5, 1.0, start=1, num=4)) 305 | eq_([], 306 | self.redis.zrevrangebyscore(key, 3.5, 1.0, start=3, num=4)) 307 | 308 | def test_zrevrangebyscore_inclusive(self): 309 | key = "zset" 310 | eq_([], self.redis.zrevrangebyscore(key, "inf", "-inf")) 311 | self.redis.zadd(key, "one", 1.5) 312 | self.redis.zadd(key, "two", 2.5) 313 | self.redis.zadd(key, "three", 3.5) 314 | self.redis.zadd(key, "four", 4.5) 315 | 316 | eq_([b"one"], 317 | self.redis.zrevrangebyscore(key, '(2.5', '1.0')) 318 | eq_([b"two", b"one"], 319 | self.redis.zrevrangebyscore(key, '(3.5', 1.0)) 320 | eq_([b"two"], 321 | self.redis.zrevrangebyscore(key, 3.0, '(1.5', start=0, num=1)) 322 | eq_([b"one"], 323 | self.redis.zrevrangebyscore(key, '(3.5', 1.0, start=1, num=1)) 324 | eq_([b"two", b"one"], 325 | self.redis.zrevrangebyscore(key, '(4.5', 1.0, start=1, num=4)) 326 | eq_([], 327 | self.redis.zrevrangebyscore(key, '(4.5', 1.0, start=3, num=4)) 328 | 329 | def test_zremrangebyrank(self): 330 | key = "zset" 331 | eq_(0, self.redis.zremrangebyrank(key, 0, -1)) 332 | 333 | self.redis.zadd(key, "one", 1.0) 334 | self.redis.zadd(key, "two", 2.0) 335 | self.redis.zadd(key, "three", 3.0) 336 | 337 | eq_(2, self.redis.zremrangebyrank(key, 0, 1)) 338 | 339 | eq_([b"three"], self.redis.zrange(key, 0, -1)) 340 | eq_(1, self.redis.zremrangebyrank(key, 0, -1)) 341 | 342 | eq_([], self.redis.zrange(key, 0, -1)) 343 | eq_([], self.redis.keys("*")) 344 | 345 | def test_zremrangebyscore(self): 346 | key = "zset" 347 | eq_(0, self.redis.zremrangebyscore(key, "-inf", "inf")) 348 | 349 | self.redis.zadd(key, "one", 1.0) 350 | self.redis.zadd(key, "two", 2.0) 351 | self.redis.zadd(key, "three", 3.0) 352 | 353 | eq_(1, self.redis.zremrangebyscore(key, 0, 1)) 354 | 355 | eq_([b"two", b"three"], self.redis.zrange(key, 0, -1)) 356 | eq_(2, self.redis.zremrangebyscore(key, 2.0, "inf")) 357 | 358 | eq_([], self.redis.zrange(key, 0, -1)) 359 | eq_([], self.redis.keys("*")) 360 | 361 | def test_zremrangebyscore_inclusive(self): 362 | key = "zset" 363 | eq_(0, self.redis.zremrangebyscore(key, "-inf", "inf")) 364 | 365 | self.redis.zadd(key, "one", 1.0) 366 | self.redis.zadd(key, "two", 2.0) 367 | self.redis.zadd(key, "three", 3.0) 368 | self.redis.zadd(key, "four", 4.0) 369 | self.redis.zadd(key, "five", 5.0) 370 | 371 | eq_(0, self.redis.zremrangebyscore(key, 0, '(1')) 372 | eq_(1, self.redis.zremrangebyscore(key, 0, '(2')) # remove "one" 373 | 374 | eq_([b"two", b"three", b"four", b"five"], self.redis.zrange(key, 0, -1)) 375 | 376 | eq_(2, self.redis.zremrangebyscore(key, '(2', 4)) # remove "three" & "four" 377 | eq_([b"two", b"five"], self.redis.zrange(key, 0, -1)) 378 | 379 | eq_(1, self.redis.zremrangebyscore(key, "(2.0", "inf")) 380 | 381 | eq_([b"two"], self.redis.zrange(key, 0, -1)) 382 | eq_(1, self.redis.zremrangebyscore(key, "-inf", "(3")) 383 | eq_([], self.redis.keys("*")) 384 | 385 | def test_zunionstore_no_keys(self): 386 | key = "zset" 387 | 388 | eq_(0, self.redis.zunionstore(key, ["zset1", "zset2"])) 389 | 390 | def test_zunionstore_default(self): 391 | # sum is default 392 | key = "zset" 393 | self.redis.zadd("zset1", "one", 1.0) 394 | self.redis.zadd("zset1", "two", 2.0) 395 | self.redis.zadd("zset2", "two", 2.5) 396 | self.redis.zadd("zset2", "three", 3.0) 397 | 398 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"])) 399 | eq_([(b"one", 1.0), (b"three", 3.0), (b"two", 4.5)], 400 | self.redis.zrange(key, 0, -1, withscores=True)) 401 | 402 | def test_zunionstore_sum(self): 403 | key = "zset" 404 | self.redis.zadd("zset1", "one", 1.0) 405 | self.redis.zadd("zset1", "two", 2.0) 406 | self.redis.zadd("zset2", "two", 2.5) 407 | self.redis.zadd("zset2", "three", 3.0) 408 | 409 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"], aggregate="sum")) 410 | eq_([(b"one", 1.0), (b"three", 3.0), (b"two", 4.5)], 411 | self.redis.zrange(key, 0, -1, withscores=True)) 412 | 413 | def test_zunionstore_SUM(self): 414 | key = "zset" 415 | self.redis.zadd("zset1", "one", 1.0) 416 | self.redis.zadd("zset1", "two", 2.0) 417 | self.redis.zadd("zset2", "two", 2.5) 418 | self.redis.zadd("zset2", "three", 3.0) 419 | 420 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"], aggregate="SUM")) 421 | eq_([(b"one", 1.0), (b"three", 3.0), (b"two", 4.5)], 422 | self.redis.zrange(key, 0, -1, withscores=True)) 423 | 424 | def test_zunionstore_min(self): 425 | key = "zset" 426 | self.redis.zadd("zset1", "one", 1.0) 427 | self.redis.zadd("zset1", "two", 2.0) 428 | self.redis.zadd("zset2", "two", 2.5) 429 | self.redis.zadd("zset2", "three", 3.0) 430 | 431 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"], aggregate="min")) 432 | eq_([(b"one", 1.0), (b"two", 2.0), (b"three", 3.0)], 433 | self.redis.zrange(key, 0, -1, withscores=True)) 434 | 435 | def test_zunionstore_MIN(self): 436 | key = "zset" 437 | self.redis.zadd("zset1", "one", 1.0) 438 | self.redis.zadd("zset1", "two", 2.0) 439 | self.redis.zadd("zset2", "two", 2.5) 440 | self.redis.zadd("zset2", "three", 3.0) 441 | 442 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"], aggregate="MIN")) 443 | eq_([(b"one", 1.0), (b"two", 2.0), (b"three", 3.0)], 444 | self.redis.zrange(key, 0, -1, withscores=True)) 445 | 446 | def test_zunionstore_max(self): 447 | key = "zset" 448 | self.redis.zadd("zset1", "one", 1.0) 449 | self.redis.zadd("zset1", "two", 2.0) 450 | self.redis.zadd("zset2", "two", 2.5) 451 | self.redis.zadd("zset2", "three", 3.0) 452 | 453 | key = "zset" 454 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"], aggregate="max")) 455 | eq_([(b"one", 1.0), (b"two", 2.5), (b"three", 3.0)], 456 | self.redis.zrange(key, 0, -1, withscores=True)) 457 | 458 | def test_zunionstore_MAX(self): 459 | key = "zset" 460 | self.redis.zadd("zset1", "one", 1.0) 461 | self.redis.zadd("zset1", "two", 2.0) 462 | self.redis.zadd("zset2", "two", 2.5) 463 | self.redis.zadd("zset2", "three", 3.0) 464 | 465 | key = "zset" 466 | eq_(3, self.redis.zunionstore(key, ["zset1", "zset2"], aggregate="MAX")) 467 | eq_([(b"one", 1.0), (b"two", 2.5), (b"three", 3.0)], 468 | self.redis.zrange(key, 0, -1, withscores=True)) 469 | 470 | def test_zinterstore_no_keys(self): 471 | key = "zset" 472 | 473 | # no keys 474 | eq_(0, self.redis.zinterstore(key, ["zset1", "zset2"])) 475 | 476 | def test_zinterstore_default(self): 477 | # sum is default 478 | key = "zset" 479 | self.redis.zadd("zset1", "one", 1.0) 480 | self.redis.zadd("zset1", "two", 2.0) 481 | self.redis.zadd("zset2", "two", 2.5) 482 | self.redis.zadd("zset2", "three", 3.0) 483 | 484 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"])) 485 | eq_([(b"two", 4.5)], 486 | self.redis.zrange(key, 0, -1, withscores=True)) 487 | 488 | def test_zinterstore_sum(self): 489 | key = "zset" 490 | self.redis.zadd("zset1", "one", 1.0) 491 | self.redis.zadd("zset1", "two", 2.0) 492 | self.redis.zadd("zset2", "two", 2.5) 493 | self.redis.zadd("zset2", "three", 3.0) 494 | 495 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"], aggregate="sum")) 496 | eq_([(b"two", 4.5)], 497 | self.redis.zrange(key, 0, -1, withscores=True)) 498 | 499 | def test_zinterstore_SUM(self): 500 | key = "zset" 501 | self.redis.zadd("zset1", "one", 1.0) 502 | self.redis.zadd("zset1", "two", 2.0) 503 | self.redis.zadd("zset2", "two", 2.5) 504 | self.redis.zadd("zset2", "three", 3.0) 505 | 506 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"], aggregate="SUM")) 507 | eq_([(b"two", 4.5)], 508 | self.redis.zrange(key, 0, -1, withscores=True)) 509 | 510 | def test_zinterstore_min(self): 511 | key = "zset" 512 | self.redis.zadd("zset1", "one", 1.0) 513 | self.redis.zadd("zset1", "two", 2.0) 514 | self.redis.zadd("zset2", "two", 2.5) 515 | self.redis.zadd("zset2", "three", 3.0) 516 | 517 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"], aggregate="min")) 518 | eq_([(b"two", 2.0)], 519 | self.redis.zrange(key, 0, -1, withscores=True)) 520 | 521 | def test_zinterstore_MIN(self): 522 | key = "zset" 523 | self.redis.zadd("zset1", "one", 1.0) 524 | self.redis.zadd("zset1", "two", 2.0) 525 | self.redis.zadd("zset2", "two", 2.5) 526 | self.redis.zadd("zset2", "three", 3.0) 527 | 528 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"], aggregate="MIN")) 529 | eq_([(b"two", 2.0)], 530 | self.redis.zrange(key, 0, -1, withscores=True)) 531 | 532 | def test_zinterstore_max(self): 533 | key = "zset" 534 | self.redis.zadd("zset1", "one", 1.0) 535 | self.redis.zadd("zset1", "two", 2.0) 536 | self.redis.zadd("zset2", "two", 2.5) 537 | self.redis.zadd("zset2", "three", 3.0) 538 | 539 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"], aggregate="max")) 540 | eq_([(b"two", 2.5)], 541 | self.redis.zrange(key, 0, -1, withscores=True)) 542 | 543 | def test_zinterstore_MAX(self): 544 | key = "zset" 545 | self.redis.zadd("zset1", "one", 1.0) 546 | self.redis.zadd("zset1", "two", 2.0) 547 | self.redis.zadd("zset2", "two", 2.5) 548 | self.redis.zadd("zset2", "three", 3.0) 549 | 550 | eq_(1, self.redis.zinterstore(key, ["zset1", "zset2"], aggregate="MAX")) 551 | eq_([(b"two", 2.5)], 552 | self.redis.zrange(key, 0, -1, withscores=True)) 553 | -------------------------------------------------------------------------------- /mockredis/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from collections import defaultdict 3 | from copy import deepcopy 4 | from itertools import chain 5 | from datetime import datetime, timedelta 6 | from hashlib import sha1 7 | from operator import add 8 | from random import choice, sample 9 | import re 10 | import sys 11 | import time 12 | import fnmatch 13 | 14 | from mockredis.clock import SystemClock 15 | from mockredis.lock import MockRedisLock 16 | from mockredis.exceptions import RedisError, ResponseError, WatchError 17 | from mockredis.pipeline import MockRedisPipeline 18 | from mockredis.script import Script 19 | from mockredis.sortedset import SortedSet 20 | from mockredis.pubsub import Pubsub 21 | 22 | if sys.version_info >= (3, 0): 23 | long = int 24 | xrange = range 25 | basestring = str 26 | from functools import reduce 27 | 28 | 29 | class MockRedis(object): 30 | """ 31 | A Mock for a redis-py Redis object 32 | 33 | Expire functionality must be explicitly 34 | invoked using do_expire(time). Automatic 35 | expiry is NOT supported. 36 | """ 37 | 38 | def __init__(self, 39 | strict=False, 40 | clock=None, 41 | load_lua_dependencies=True, 42 | blocking_timeout=1000, 43 | blocking_sleep_interval=0.01, 44 | **kwargs): 45 | """ 46 | Initialize as either StrictRedis or Redis. 47 | 48 | Defaults to non-strict. 49 | """ 50 | self.strict = strict 51 | self.clock = SystemClock() if clock is None else clock 52 | self.load_lua_dependencies = load_lua_dependencies 53 | self.blocking_timeout = blocking_timeout 54 | self.blocking_sleep_interval = blocking_sleep_interval 55 | # The 'Redis' store 56 | self.redis = defaultdict(dict) 57 | self.redis_config = defaultdict(dict) 58 | self.timeouts = defaultdict(dict) 59 | # Dictionary from script to sha ''Script'' 60 | self.shas = dict() 61 | self._pubsub = None 62 | 63 | @classmethod 64 | def from_url(cls, url, db=None, **kwargs): 65 | return cls(**kwargs) 66 | 67 | # Connection Functions # 68 | 69 | def echo(self, msg): 70 | return self._encode(msg) 71 | 72 | def ping(self): 73 | return b'PONG' 74 | 75 | # Transactions Functions # 76 | 77 | def lock(self, key, timeout=0, sleep=0): 78 | """Emulate lock.""" 79 | return MockRedisLock(self, key, timeout, sleep) 80 | 81 | def pipeline(self, transaction=True, shard_hint=None): 82 | """Emulate a redis-python pipeline.""" 83 | return MockRedisPipeline(self, transaction, shard_hint) 84 | 85 | def transaction(self, func, *watches, **kwargs): 86 | """ 87 | Convenience method for executing the callable `func` as a transaction 88 | while watching all keys specified in `watches`. The 'func' callable 89 | should expect a single argument which is a Pipeline object. 90 | 91 | Copied directly from redis-py. 92 | """ 93 | shard_hint = kwargs.pop('shard_hint', None) 94 | value_from_callable = kwargs.pop('value_from_callable', False) 95 | watch_delay = kwargs.pop('watch_delay', None) 96 | with self.pipeline(True, shard_hint) as pipe: 97 | while 1: 98 | try: 99 | if watches: 100 | pipe.watch(*watches) 101 | func_value = func(pipe) 102 | exec_value = pipe.execute() 103 | return func_value if value_from_callable else exec_value 104 | except WatchError: 105 | if watch_delay is not None and watch_delay > 0: 106 | time.sleep(watch_delay) 107 | continue 108 | 109 | def watch(self, *argv, **kwargs): 110 | """ 111 | Mock does not support command buffering so watch 112 | is a no-op 113 | """ 114 | pass 115 | 116 | def unwatch(self): 117 | """ 118 | Mock does not support command buffering so unwatch 119 | is a no-op 120 | """ 121 | pass 122 | 123 | def multi(self, *argv, **kwargs): 124 | """ 125 | Mock does not support command buffering so multi 126 | is a no-op 127 | """ 128 | pass 129 | 130 | def execute(self): 131 | """Emulate the execute method. All piped commands are executed immediately 132 | in this mock, so this is a no-op.""" 133 | pass 134 | 135 | # Keys Functions # 136 | 137 | def type(self, key): 138 | key = self._encode(key) 139 | if key not in self.redis: 140 | return b'none' 141 | type_ = type(self.redis[key]) 142 | if type_ is dict: 143 | return b'hash' 144 | elif type_ is str: 145 | return b'string' 146 | elif type_ is set: 147 | return b'set' 148 | elif type_ is list: 149 | return b'list' 150 | elif type_ is SortedSet: 151 | return b'zset' 152 | raise TypeError("unhandled type {}".format(type_)) 153 | 154 | def keys(self, pattern='*'): 155 | """Emulate keys.""" 156 | # making sure the pattern is unicode/str. 157 | try: 158 | pattern = pattern.decode('utf-8') 159 | # This throws an AttributeError in python 3, or an 160 | # UnicodeEncodeError in python 2 161 | except (AttributeError, UnicodeEncodeError): 162 | pass 163 | 164 | # Make regex out of glob styled pattern. 165 | regex = fnmatch.translate(pattern) 166 | regex = re.compile(re.sub(r'(^|[^\\])\.', r'\1[^/]', regex)) 167 | 168 | # Find every key that matches the pattern 169 | return [key for key in self.redis.keys() if regex.match(key.decode('utf-8'))] 170 | 171 | def delete(self, *keys): 172 | """Emulate delete.""" 173 | key_counter = 0 174 | for key in map(self._encode, keys): 175 | if key in self.redis: 176 | del self.redis[key] 177 | key_counter += 1 178 | if key in self.timeouts: 179 | del self.timeouts[key] 180 | return key_counter 181 | 182 | def __delitem__(self, name): 183 | if self.delete(name) == 0: 184 | # redispy doesn't correctly raise KeyError here, so we don't either 185 | pass 186 | 187 | def exists(self, key): 188 | """Emulate exists.""" 189 | return self._encode(key) in self.redis 190 | __contains__ = exists 191 | 192 | def _expire(self, key, delta): 193 | if key not in self.redis: 194 | return False 195 | 196 | self.timeouts[key] = self.clock.now() + delta 197 | return True 198 | 199 | def expire(self, key, delta): 200 | """Emulate expire""" 201 | delta = delta if isinstance(delta, timedelta) else timedelta(seconds=delta) 202 | return self._expire(self._encode(key), delta) 203 | 204 | def pexpire(self, key, milliseconds): 205 | """Emulate pexpire""" 206 | return self._expire(self._encode(key), timedelta(milliseconds=milliseconds)) 207 | 208 | def expireat(self, key, when): 209 | """Emulate expireat""" 210 | expire_time = datetime.fromtimestamp(when) 211 | key = self._encode(key) 212 | if key in self.redis: 213 | self.timeouts[key] = expire_time 214 | return True 215 | return False 216 | 217 | def ttl(self, key): 218 | """ 219 | Emulate ttl 220 | 221 | Even though the official redis commands documentation at http://redis.io/commands/ttl 222 | states "Return value: Integer reply: TTL in seconds, -2 when key does not exist or -1 223 | when key does not have a timeout." the redis-py lib returns None for both these cases. 224 | The lib behavior has been emulated here. 225 | 226 | :param key: key for which ttl is requested. 227 | :returns: the number of seconds till timeout, None if the key does not exist or if the 228 | key has no timeout(as per the redis-py lib behavior). 229 | """ 230 | value = self.pttl(key) 231 | if value is None or value < 0: 232 | return value 233 | return value // 1000 234 | 235 | def pttl(self, key): 236 | """ 237 | Emulate pttl 238 | 239 | :param key: key for which pttl is requested. 240 | :returns: the number of milliseconds till timeout, None if the key does not exist or if the 241 | key has no timeout(as per the redis-py lib behavior). 242 | """ 243 | """ 244 | Returns time to live in milliseconds if output_ms is True, else returns seconds. 245 | """ 246 | key = self._encode(key) 247 | if key not in self.redis: 248 | # as of redis 2.8, -2 is returned if the key does not exist 249 | return long(-2) if self.strict else None 250 | if key not in self.timeouts: 251 | # as of redis 2.8, -1 is returned if the key is persistent 252 | # redis-py returns None; command docs say -1 253 | return long(-1) if self.strict else None 254 | 255 | time_to_live = get_total_milliseconds(self.timeouts[key] - self.clock.now()) 256 | return long(max(-1, time_to_live)) 257 | 258 | def do_expire(self): 259 | """ 260 | Expire objects assuming now == time 261 | """ 262 | # Deep copy to avoid RuntimeError: dictionary changed size during iteration 263 | _timeouts = deepcopy(self.timeouts) 264 | for key, value in _timeouts.items(): 265 | if value - self.clock.now() < timedelta(0): 266 | del self.timeouts[key] 267 | # removing the expired key 268 | if key in self.redis: 269 | self.redis.pop(key, None) 270 | 271 | def flushdb(self): 272 | self.redis.clear() 273 | self.pubsub().clear() 274 | self.timeouts.clear() 275 | 276 | def rename(self, old_key, new_key): 277 | return self._rename(old_key, new_key) 278 | 279 | def renamenx(self, old_key, new_key): 280 | return 1 if self._rename(old_key, new_key, True) else 0 281 | 282 | def _rename(self, old_key, new_key, nx=False): 283 | old_key = self._encode(old_key) 284 | new_key = self._encode(new_key) 285 | if old_key in self.redis and (not nx or new_key not in self.redis): 286 | self.redis[new_key] = self.redis.pop(old_key) 287 | return True 288 | return False 289 | 290 | def dbsize(self): 291 | return len(self.redis.keys()) 292 | 293 | # String Functions # 294 | 295 | def get(self, key): 296 | key = self._encode(key) 297 | return self.redis.get(key) 298 | 299 | def __getitem__(self, name): 300 | """ 301 | Return the value at key ``name``, raises a KeyError if the key 302 | doesn't exist. 303 | """ 304 | value = self.get(name) 305 | if value is not None: 306 | return value 307 | raise KeyError(name) 308 | 309 | def mget(self, keys, *args): 310 | args = self._list_or_args(keys, args) 311 | return [self.get(arg) for arg in args] 312 | 313 | def set(self, key, value, ex=None, px=None, nx=False, xx=False): 314 | """ 315 | Set the ``value`` for the ``key`` in the context of the provided kwargs. 316 | 317 | As per the behavior of the redis-py lib: 318 | If nx and xx are both set, the function does nothing and None is returned. 319 | If px and ex are both set, the preference is given to px. 320 | If the key is not set for some reason, the lib function returns None. 321 | """ 322 | key = self._encode(key) 323 | value = self._encode(value) 324 | 325 | if nx and xx: 326 | return None 327 | mode = "nx" if nx else "xx" if xx else None 328 | if self._should_set(key, mode): 329 | expire = None 330 | if ex is not None: 331 | expire = ex if isinstance(ex, timedelta) else timedelta(seconds=ex) 332 | if px is not None: 333 | expire = px if isinstance(px, timedelta) else timedelta(milliseconds=px) 334 | 335 | if expire is not None and expire.total_seconds() <= 0: 336 | raise ResponseError("invalid expire time in SETEX") 337 | 338 | result = self._set(key, value) 339 | if expire: 340 | self._expire(key, expire) 341 | 342 | return result 343 | __setitem__ = set 344 | 345 | def getset(self, key, value): 346 | old_value = self.get(key) 347 | self.set(key, value) 348 | return old_value 349 | 350 | def _set(self, key, value): 351 | self.redis[key] = self._encode(value) 352 | 353 | # removing the timeout 354 | if key in self.timeouts: 355 | self.timeouts.pop(key, None) 356 | 357 | return True 358 | 359 | def _should_set(self, key, mode): 360 | """ 361 | Determine if it is okay to set a key. 362 | 363 | If the mode is None, returns True, otherwise, returns True of false based on 364 | the value of ``key`` and the ``mode`` (nx | xx). 365 | """ 366 | 367 | if mode is None or mode not in ["nx", "xx"]: 368 | return True 369 | 370 | if mode == "nx": 371 | if key in self.redis: 372 | # nx means set only if key is absent 373 | # false if the key already exists 374 | return False 375 | elif key not in self.redis: 376 | # at this point mode can only be xx 377 | # xx means set only if the key already exists 378 | # false if is absent 379 | return False 380 | # for all other cases, return true 381 | return True 382 | 383 | def setex(self, key, time, value): 384 | """ 385 | Set the value of ``key`` to ``value`` that expires in ``time`` 386 | seconds. ``time`` can be represented by an integer or a Python 387 | timedelta object. 388 | """ 389 | if not self.strict: 390 | # when not strict mode swap value and time args order 391 | time, value = value, time 392 | return self.set(key, value, ex=time) 393 | 394 | def psetex(self, key, time, value): 395 | """ 396 | Set the value of ``key`` to ``value`` that expires in ``time`` 397 | milliseconds. ``time`` can be represented by an integer or a Python 398 | timedelta object. 399 | """ 400 | return self.set(key, value, px=time) 401 | 402 | def setnx(self, key, value): 403 | """Set the value of ``key`` to ``value`` if key doesn't exist""" 404 | return self.set(key, value, nx=True) 405 | 406 | def mset(self, *args, **kwargs): 407 | """ 408 | Sets key/values based on a mapping. Mapping can be supplied as a single 409 | dictionary argument or as kwargs. 410 | """ 411 | if args: 412 | if len(args) != 1 or not isinstance(args[0], dict): 413 | raise RedisError('MSET requires **kwargs or a single dict arg') 414 | mapping = args[0] 415 | else: 416 | mapping = kwargs 417 | for key, value in mapping.items(): 418 | self.set(key, value) 419 | return True 420 | 421 | def msetnx(self, *args, **kwargs): 422 | """ 423 | Sets key/values based on a mapping if none of the keys are already set. 424 | Mapping can be supplied as a single dictionary argument or as kwargs. 425 | Returns a boolean indicating if the operation was successful. 426 | """ 427 | if args: 428 | if len(args) != 1 or not isinstance(args[0], dict): 429 | raise RedisError('MSETNX requires **kwargs or a single dict arg') 430 | mapping = args[0] 431 | else: 432 | mapping = kwargs 433 | 434 | for key in mapping.keys(): 435 | if self._encode(key) in self.redis: 436 | return False 437 | for key, value in mapping.items(): 438 | self.set(key, value) 439 | 440 | return True 441 | 442 | def decr(self, key, amount=1): 443 | key = self._encode(key) 444 | previous_value = long(self.redis.get(key, '0')) 445 | self.redis[key] = self._encode(previous_value - amount) 446 | return long(self.redis[key]) 447 | 448 | decrby = decr 449 | 450 | def incr(self, key, amount=1): 451 | """Emulate incr.""" 452 | key = self._encode(key) 453 | previous_value = long(self.redis.get(key, '0')) 454 | self.redis[key] = self._encode(previous_value + amount) 455 | return long(self.redis[key]) 456 | 457 | incrby = incr 458 | 459 | def setbit(self, key, offset, value): 460 | """ 461 | Set the bit at ``offset`` in ``key`` to ``value``. 462 | """ 463 | key = self._encode(key) 464 | index, bits, mask = self._get_bits_and_offset(key, offset) 465 | 466 | if index >= len(bits): 467 | bits.extend(b"\x00" * (index + 1 - len(bits))) 468 | 469 | prev_val = 1 if (bits[index] & mask) else 0 470 | 471 | if value: 472 | bits[index] |= mask 473 | else: 474 | bits[index] &= ~mask 475 | 476 | self.redis[key] = bytes(bits) 477 | 478 | return prev_val 479 | 480 | def getbit(self, key, offset): 481 | """ 482 | Returns the bit value at ``offset`` in ``key``. 483 | """ 484 | key = self._encode(key) 485 | index, bits, mask = self._get_bits_and_offset(key, offset) 486 | 487 | if index >= len(bits): 488 | return 0 489 | 490 | return 1 if (bits[index] & mask) else 0 491 | 492 | def _get_bits_and_offset(self, key, offset): 493 | bits = bytearray(self.redis.get(key, b"")) 494 | index, position = divmod(offset, 8) 495 | mask = 128 >> position 496 | return index, bits, mask 497 | 498 | # Hash Functions # 499 | 500 | def hexists(self, hashkey, attribute): 501 | """Emulate hexists.""" 502 | 503 | redis_hash = self._get_hash(hashkey, 'HEXISTS') 504 | return self._encode(attribute) in redis_hash 505 | 506 | def hget(self, hashkey, attribute): 507 | """Emulate hget.""" 508 | 509 | redis_hash = self._get_hash(hashkey, 'HGET') 510 | return redis_hash.get(self._encode(attribute)) 511 | 512 | def hgetall(self, hashkey): 513 | """Emulate hgetall.""" 514 | 515 | redis_hash = self._get_hash(hashkey, 'HGETALL') 516 | return dict(redis_hash) 517 | 518 | def hdel(self, hashkey, *keys): 519 | """Emulate hdel""" 520 | 521 | redis_hash = self._get_hash(hashkey, 'HDEL') 522 | count = 0 523 | for key in keys: 524 | attribute = self._encode(key) 525 | if attribute in redis_hash: 526 | count += 1 527 | del redis_hash[attribute] 528 | if not redis_hash: 529 | self.delete(hashkey) 530 | return count 531 | 532 | def hlen(self, hashkey): 533 | """Emulate hlen.""" 534 | redis_hash = self._get_hash(hashkey, 'HLEN') 535 | return len(redis_hash) 536 | 537 | def hmset(self, hashkey, value): 538 | """Emulate hmset.""" 539 | 540 | redis_hash = self._get_hash(hashkey, 'HMSET', create=True) 541 | for key, value in value.items(): 542 | attribute = self._encode(key) 543 | redis_hash[attribute] = self._encode(value) 544 | return True 545 | 546 | def hmget(self, hashkey, keys, *args): 547 | """Emulate hmget.""" 548 | 549 | redis_hash = self._get_hash(hashkey, 'HMGET') 550 | attributes = self._list_or_args(keys, args) 551 | return [redis_hash.get(self._encode(attribute)) for attribute in attributes] 552 | 553 | def hset(self, hashkey, attribute, value): 554 | """Emulate hset.""" 555 | 556 | redis_hash = self._get_hash(hashkey, 'HSET', create=True) 557 | attribute = self._encode(attribute) 558 | attribute_present = attribute in redis_hash 559 | redis_hash[attribute] = self._encode(value) 560 | return long(0) if attribute_present else long(1) 561 | 562 | def hsetnx(self, hashkey, attribute, value): 563 | """Emulate hsetnx.""" 564 | 565 | redis_hash = self._get_hash(hashkey, 'HSETNX', create=True) 566 | attribute = self._encode(attribute) 567 | if attribute in redis_hash: 568 | return long(0) 569 | else: 570 | redis_hash[attribute] = self._encode(value) 571 | return long(1) 572 | 573 | def hincrby(self, hashkey, attribute, increment=1): 574 | """Emulate hincrby.""" 575 | 576 | return self._hincrby(hashkey, attribute, 'HINCRBY', long, increment) 577 | 578 | def hincrbyfloat(self, hashkey, attribute, increment=1.0): 579 | """Emulate hincrbyfloat.""" 580 | 581 | return self._hincrby(hashkey, attribute, 'HINCRBYFLOAT', float, increment) 582 | 583 | def _hincrby(self, hashkey, attribute, command, type_, increment): 584 | """Shared hincrby and hincrbyfloat routine""" 585 | redis_hash = self._get_hash(hashkey, command, create=True) 586 | attribute = self._encode(attribute) 587 | previous_value = type_(redis_hash.get(attribute, '0')) 588 | redis_hash[attribute] = self._encode(previous_value + increment) 589 | return type_(redis_hash[attribute]) 590 | 591 | def hkeys(self, hashkey): 592 | """Emulate hkeys.""" 593 | 594 | redis_hash = self._get_hash(hashkey, 'HKEYS') 595 | return redis_hash.keys() 596 | 597 | def hvals(self, hashkey): 598 | """Emulate hvals.""" 599 | 600 | redis_hash = self._get_hash(hashkey, 'HVALS') 601 | return redis_hash.values() 602 | 603 | # List Functions # 604 | 605 | def lrange(self, key, start, stop): 606 | """Emulate lrange.""" 607 | redis_list = self._get_list(key, 'LRANGE') 608 | start, stop = self._translate_range(len(redis_list), start, stop) 609 | return redis_list[start:stop + 1] 610 | 611 | def lindex(self, key, index): 612 | """Emulate lindex.""" 613 | 614 | redis_list = self._get_list(key, 'LINDEX') 615 | 616 | if self._encode(key) not in self.redis: 617 | return None 618 | 619 | try: 620 | return redis_list[index] 621 | except (IndexError): 622 | # Redis returns nil if the index doesn't exist 623 | return None 624 | 625 | def llen(self, key): 626 | """Emulate llen.""" 627 | redis_list = self._get_list(key, 'LLEN') 628 | 629 | # Redis returns 0 if list doesn't exist 630 | return len(redis_list) 631 | 632 | def _blocking_pop(self, pop_func, keys, timeout): 633 | """Emulate blocking pop functionality""" 634 | if not isinstance(timeout, (int, long)): 635 | raise RuntimeError('timeout is not an integer or out of range') 636 | 637 | if timeout is None or timeout == 0: 638 | timeout = self.blocking_timeout 639 | 640 | if isinstance(keys, basestring): 641 | keys = [keys] 642 | else: 643 | keys = list(keys) 644 | 645 | elapsed_time = 0 646 | start = time.time() 647 | while elapsed_time < timeout: 648 | key, val = self._pop_first_available(pop_func, keys) 649 | if val: 650 | return key, val 651 | # small delay to avoid high cpu utilization 652 | time.sleep(self.blocking_sleep_interval) 653 | elapsed_time = time.time() - start 654 | return None 655 | 656 | def _pop_first_available(self, pop_func, keys): 657 | for key in keys: 658 | val = pop_func(key) 659 | if val: 660 | return self._encode(key), val 661 | return None, None 662 | 663 | def blpop(self, keys, timeout=0): 664 | """Emulate blpop""" 665 | return self._blocking_pop(self.lpop, keys, timeout) 666 | 667 | def brpop(self, keys, timeout=0): 668 | """Emulate brpop""" 669 | return self._blocking_pop(self.rpop, keys, timeout) 670 | 671 | def lpop(self, key): 672 | """Emulate lpop.""" 673 | redis_list = self._get_list(key, 'LPOP') 674 | 675 | if self._encode(key) not in self.redis: 676 | return None 677 | 678 | try: 679 | value = redis_list.pop(0) 680 | if len(redis_list) == 0: 681 | self.delete(key) 682 | return value 683 | except (IndexError): 684 | # Redis returns nil if popping from an empty list 685 | return None 686 | 687 | def lpush(self, key, *args): 688 | """Emulate lpush.""" 689 | redis_list = self._get_list(key, 'LPUSH', create=True) 690 | 691 | # Creates the list at this key if it doesn't exist, and appends args to its beginning 692 | args_reversed = [self._encode(arg) for arg in args] 693 | args_reversed.reverse() 694 | updated_list = args_reversed + redis_list 695 | self.redis[self._encode(key)] = updated_list 696 | 697 | # Return the length of the list after the push operation 698 | return len(updated_list) 699 | 700 | def rpop(self, key): 701 | """Emulate lpop.""" 702 | redis_list = self._get_list(key, 'RPOP') 703 | 704 | if self._encode(key) not in self.redis: 705 | return None 706 | 707 | try: 708 | value = redis_list.pop() 709 | if len(redis_list) == 0: 710 | self.delete(key) 711 | return value 712 | except (IndexError): 713 | # Redis returns nil if popping from an empty list 714 | return None 715 | 716 | def rpush(self, key, *args): 717 | """Emulate rpush.""" 718 | redis_list = self._get_list(key, 'RPUSH', create=True) 719 | 720 | # Creates the list at this key if it doesn't exist, and appends args to it 721 | redis_list.extend(map(self._encode, args)) 722 | 723 | # Return the length of the list after the push operation 724 | return len(redis_list) 725 | 726 | def lrem(self, key, value, count=0): 727 | """Emulate lrem.""" 728 | value = self._encode(value) 729 | redis_list = self._get_list(key, 'LREM') 730 | removed_count = 0 731 | if self._encode(key) in self.redis: 732 | if count == 0: 733 | # Remove all ocurrences 734 | while redis_list.count(value): 735 | redis_list.remove(value) 736 | removed_count += 1 737 | elif count > 0: 738 | counter = 0 739 | # remove first 'count' ocurrences 740 | while redis_list.count(value): 741 | redis_list.remove(value) 742 | counter += 1 743 | removed_count += 1 744 | if counter >= count: 745 | break 746 | elif count < 0: 747 | # remove last 'count' ocurrences 748 | counter = -count 749 | new_list = [] 750 | for v in reversed(redis_list): 751 | if v == value and counter > 0: 752 | counter -= 1 753 | removed_count += 1 754 | else: 755 | new_list.append(v) 756 | redis_list[:] = list(reversed(new_list)) 757 | if removed_count > 0 and len(redis_list) == 0: 758 | self.delete(key) 759 | return removed_count 760 | 761 | def ltrim(self, key, start, stop): 762 | """Emulate ltrim.""" 763 | redis_list = self._get_list(key, 'LTRIM') 764 | if redis_list: 765 | start, stop = self._translate_range(len(redis_list), start, stop) 766 | self.redis[self._encode(key)] = redis_list[start:stop + 1] 767 | return True 768 | 769 | def rpoplpush(self, source, destination): 770 | """Emulate rpoplpush""" 771 | transfer_item = self.rpop(source) 772 | if transfer_item is not None: 773 | self.lpush(destination, transfer_item) 774 | return transfer_item 775 | 776 | def brpoplpush(self, source, destination, timeout=0): 777 | """Emulate brpoplpush""" 778 | transfer_item = self.brpop(source, timeout) 779 | if transfer_item is None: 780 | return None 781 | 782 | key, val = transfer_item 783 | self.lpush(destination, val) 784 | return val 785 | 786 | def lset(self, key, index, value): 787 | """Emulate lset.""" 788 | redis_list = self._get_list(key, 'LSET') 789 | if redis_list is None: 790 | raise ResponseError("no such key") 791 | try: 792 | redis_list[index] = self._encode(value) 793 | except IndexError: 794 | raise ResponseError("index out of range") 795 | 796 | def sort(self, name, 797 | start=None, 798 | num=None, 799 | by=None, 800 | get=None, 801 | desc=False, 802 | alpha=False, 803 | store=None, 804 | groups=False): 805 | # check valid parameter combos 806 | if [start, num] != [None, None] and None in [start, num]: 807 | raise ValueError('start and num must both be specified together') 808 | 809 | # check up-front if there's anything to actually do 810 | items = num != 0 and self.get(name) 811 | if not items: 812 | if store: 813 | return 0 814 | else: 815 | return [] 816 | 817 | by = self._encode(by) if by is not None else by 818 | # always organize the items as tuples of the value from the list and the sort key 819 | if by and b'*' in by: 820 | items = [(i, self.get(by.replace(b'*', self._encode(i)))) for i in items] 821 | elif by in [None, b'nosort']: 822 | items = [(i, i) for i in items] 823 | else: 824 | raise ValueError('invalid value for "by": %s' % by) 825 | 826 | if by != b'nosort': 827 | # if sorting, do alpha sort or float (default) and take desc flag into account 828 | sort_type = alpha and str or float 829 | items.sort(key=lambda x: sort_type(x[1]), reverse=bool(desc)) 830 | 831 | # results is a list of lists to support different styles of get and also groups 832 | results = [] 833 | if get: 834 | if isinstance(get, basestring): 835 | # always deal with get specifiers as a list 836 | get = [get] 837 | for g in map(self._encode, get): 838 | if g == b'#': 839 | results.append([self.get(i) for i in items]) 840 | else: 841 | results.append([self.get(g.replace(b'*', self._encode(i[0]))) for i in items]) 842 | else: 843 | # if not using GET then returning just the item itself 844 | results.append([i[0] for i in items]) 845 | 846 | # results to either list of tuples or list of values 847 | if len(results) > 1: 848 | results = list(zip(*results)) 849 | elif results: 850 | results = results[0] 851 | 852 | # apply the 'start' and 'num' to the results 853 | if not start: 854 | start = 0 855 | if not num: 856 | if start: 857 | results = results[start:] 858 | else: 859 | end = start + num 860 | results = results[start:end] 861 | 862 | # if more than one GET then flatten if groups not wanted 863 | if get and len(get) > 1: 864 | if not groups: 865 | results = list(chain(*results)) 866 | 867 | # either store value and return length of results or just return results 868 | if store: 869 | self.redis[self._encode(store)] = results 870 | return len(results) 871 | else: 872 | return results 873 | 874 | # SCAN COMMANDS # 875 | 876 | def _common_scan(self, values_function, cursor='0', match=None, count=10, key=None): 877 | """ 878 | Common scanning skeleton. 879 | 880 | :param key: optional function used to identify what 'match' is applied to 881 | """ 882 | if count is None: 883 | count = 10 884 | cursor = int(cursor) 885 | count = int(count) 886 | if not count: 887 | raise ValueError('if specified, count must be > 0: %s' % count) 888 | 889 | values = values_function() 890 | if cursor + count >= len(values): 891 | # we reached the end, back to zero 892 | result_cursor = 0 893 | else: 894 | result_cursor = cursor + count 895 | 896 | values = values[cursor:cursor+count] 897 | 898 | if match is not None: 899 | regex = re.compile(b'^' + re.escape(self._encode(match)).replace(b'\\*', b'.*') + b'$') 900 | if not key: 901 | key = lambda v: v 902 | values = [v for v in values if regex.match(key(v))] 903 | 904 | return [result_cursor, values] 905 | 906 | def scan(self, cursor='0', match=None, count=10): 907 | """Emulate scan.""" 908 | def value_function(): 909 | return sorted(self.redis.keys()) # sorted list for consistent order 910 | return self._common_scan(value_function, cursor=cursor, match=match, count=count) 911 | 912 | def scan_iter(self, match=None, count=10): 913 | """Emulate scan_iter.""" 914 | cursor = '0' 915 | while cursor != 0: 916 | cursor, data = self.scan(cursor=cursor, match=match, count=count) 917 | for item in data: 918 | yield item 919 | 920 | def sscan(self, name, cursor='0', match=None, count=10): 921 | """Emulate sscan.""" 922 | def value_function(): 923 | members = list(self.smembers(name)) 924 | members.sort() # sort for consistent order 925 | return members 926 | return self._common_scan(value_function, cursor=cursor, match=match, count=count) 927 | 928 | def sscan_iter(self, name, match=None, count=10): 929 | """Emulate sscan_iter.""" 930 | cursor = '0' 931 | while cursor != 0: 932 | cursor, data = self.sscan(name, cursor=cursor, 933 | match=match, count=count) 934 | for item in data: 935 | yield item 936 | 937 | def zscan(self, name, cursor='0', match=None, count=10): 938 | """Emulate zscan.""" 939 | def value_function(): 940 | values = self.zrange(name, 0, -1, withscores=True) 941 | values.sort(key=lambda x: x[1]) # sort for consistent order 942 | return values 943 | return self._common_scan(value_function, cursor=cursor, match=match, count=count, key=lambda v: v[0]) # noqa 944 | 945 | def zscan_iter(self, name, match=None, count=10): 946 | """Emulate zscan_iter.""" 947 | cursor = '0' 948 | while cursor != 0: 949 | cursor, data = self.zscan(name, cursor=cursor, match=match, 950 | count=count) 951 | for item in data: 952 | yield item 953 | 954 | def hscan(self, name, cursor='0', match=None, count=10): 955 | """Emulate hscan.""" 956 | def value_function(): 957 | values = self.hgetall(name) 958 | values = list(values.items()) # list of tuples for sorting and matching 959 | values.sort(key=lambda x: x[0]) # sort for consistent order 960 | return values 961 | scanned = self._common_scan(value_function, cursor=cursor, match=match, count=count, key=lambda v: v[0]) # noqa 962 | scanned[1] = dict(scanned[1]) # from list of tuples back to dict 963 | return scanned 964 | 965 | def hscan_iter(self, name, match=None, count=10): 966 | """Emulate hscan_iter.""" 967 | cursor = '0' 968 | while cursor != 0: 969 | cursor, data = self.hscan(name, cursor=cursor, 970 | match=match, count=count) 971 | for item in data.items(): 972 | yield item 973 | 974 | # SET COMMANDS # 975 | 976 | def sadd(self, key, *values): 977 | """Emulate sadd.""" 978 | if len(values) == 0: 979 | raise ResponseError("wrong number of arguments for 'sadd' command") 980 | redis_set = self._get_set(key, 'SADD', create=True) 981 | before_count = len(redis_set) 982 | redis_set.update(map(self._encode, values)) 983 | after_count = len(redis_set) 984 | return after_count - before_count 985 | 986 | def scard(self, key): 987 | """Emulate scard.""" 988 | redis_set = self._get_set(key, 'SADD') 989 | return len(redis_set) 990 | 991 | def sdiff(self, keys, *args): 992 | """Emulate sdiff.""" 993 | func = lambda left, right: left.difference(right) 994 | return self._apply_to_sets(func, "SDIFF", keys, *args) 995 | 996 | def sdiffstore(self, dest, keys, *args): 997 | """Emulate sdiffstore.""" 998 | result = self.sdiff(keys, *args) 999 | self.redis[self._encode(dest)] = result 1000 | return len(result) 1001 | 1002 | def sinter(self, keys, *args): 1003 | """Emulate sinter.""" 1004 | func = lambda left, right: left.intersection(right) 1005 | return self._apply_to_sets(func, "SINTER", keys, *args) 1006 | 1007 | def sinterstore(self, dest, keys, *args): 1008 | """Emulate sinterstore.""" 1009 | result = self.sinter(keys, *args) 1010 | self.redis[self._encode(dest)] = result 1011 | return len(result) 1012 | 1013 | def sismember(self, name, value): 1014 | """Emulate sismember.""" 1015 | redis_set = self._get_set(name, 'SISMEMBER') 1016 | if not redis_set: 1017 | return 0 1018 | 1019 | result = self._encode(value) in redis_set 1020 | return 1 if result else 0 1021 | 1022 | def smembers(self, name): 1023 | """Emulate smembers.""" 1024 | return self._get_set(name, 'SMEMBERS').copy() 1025 | 1026 | def smove(self, src, dst, value): 1027 | """Emulate smove.""" 1028 | src_set = self._get_set(src, 'SMOVE') 1029 | dst_set = self._get_set(dst, 'SMOVE') 1030 | value = self._encode(value) 1031 | 1032 | if value not in src_set: 1033 | return False 1034 | 1035 | src_set.discard(value) 1036 | dst_set.add(value) 1037 | self.redis[self._encode(src)], self.redis[self._encode(dst)] = src_set, dst_set 1038 | return True 1039 | 1040 | def spop(self, name): 1041 | """Emulate spop.""" 1042 | redis_set = self._get_set(name, 'SPOP') 1043 | if not redis_set: 1044 | return None 1045 | member = choice(list(redis_set)) 1046 | redis_set.remove(member) 1047 | if len(redis_set) == 0: 1048 | self.delete(name) 1049 | return member 1050 | 1051 | def srandmember(self, name, number=None): 1052 | """Emulate srandmember.""" 1053 | redis_set = self._get_set(name, 'SRANDMEMBER') 1054 | if not redis_set: 1055 | return None if number is None else [] 1056 | if number is None: 1057 | return choice(list(redis_set)) 1058 | elif number > 0: 1059 | return sample(list(redis_set), min(number, len(redis_set))) 1060 | else: 1061 | return [choice(list(redis_set)) for _ in xrange(abs(number))] 1062 | 1063 | def srem(self, key, *values): 1064 | """Emulate srem.""" 1065 | redis_set = self._get_set(key, 'SREM') 1066 | if not redis_set: 1067 | return 0 1068 | before_count = len(redis_set) 1069 | for value in values: 1070 | redis_set.discard(self._encode(value)) 1071 | after_count = len(redis_set) 1072 | if before_count > 0 and len(redis_set) == 0: 1073 | self.delete(key) 1074 | return before_count - after_count 1075 | 1076 | def sunion(self, keys, *args): 1077 | """Emulate sunion.""" 1078 | func = lambda left, right: left.union(right) 1079 | return self._apply_to_sets(func, "SUNION", keys, *args) 1080 | 1081 | def sunionstore(self, dest, keys, *args): 1082 | """Emulate sunionstore.""" 1083 | result = self.sunion(keys, *args) 1084 | self.redis[self._encode(dest)] = result 1085 | return len(result) 1086 | 1087 | # SORTED SET COMMANDS # 1088 | 1089 | def zadd(self, name, *args, **kwargs): 1090 | zset = self._get_zset(name, "ZADD", create=True) 1091 | 1092 | pieces = [] 1093 | 1094 | # args 1095 | if len(args) % 2 != 0: 1096 | raise RedisError("ZADD requires an equal number of " 1097 | "values and scores") 1098 | for i in xrange(len(args) // 2): 1099 | # interpretation of args order depends on whether Redis 1100 | # or StrictRedis is used 1101 | score = args[2 * i + (0 if self.strict else 1)] 1102 | member = args[2 * i + (1 if self.strict else 0)] 1103 | pieces.append((member, score)) 1104 | 1105 | # kwargs 1106 | pieces.extend(kwargs.items()) 1107 | 1108 | insert_count = lambda member, score: 1 if zset.insert(self._encode(member), float(score)) else 0 # noqa 1109 | return sum((insert_count(member, score) for member, score in pieces)) 1110 | 1111 | def zcard(self, name): 1112 | zset = self._get_zset(name, "ZCARD") 1113 | 1114 | return len(zset) if zset is not None else 0 1115 | 1116 | def zcount(self, name, min, max): 1117 | zset = self._get_zset(name, "ZCOUNT") 1118 | 1119 | if not zset: 1120 | return 0 1121 | 1122 | return len(zset.scorerange(float(min), float(max))) 1123 | 1124 | def zincrby(self, name, value, amount=1): 1125 | zset = self._get_zset(name, "ZINCRBY", create=True) 1126 | 1127 | value = self._encode(value) 1128 | score = zset.score(value) or 0.0 1129 | score += float(amount) 1130 | zset[value] = score 1131 | return score 1132 | 1133 | def zinterstore(self, dest, keys, aggregate=None): 1134 | aggregate_func = self._aggregate_func(aggregate) 1135 | 1136 | members = {} 1137 | 1138 | for key in keys: 1139 | zset = self._get_zset(key, "ZINTERSTORE") 1140 | if not zset: 1141 | return 0 1142 | 1143 | for score, member in zset: 1144 | members.setdefault(member, []).append(score) 1145 | 1146 | intersection = SortedSet() 1147 | for member, scores in members.items(): 1148 | if len(scores) != len(keys): 1149 | continue 1150 | intersection[member] = reduce(aggregate_func, scores) 1151 | 1152 | # always override existing keys 1153 | self.redis[self._encode(dest)] = intersection 1154 | return len(intersection) 1155 | 1156 | def zrange(self, name, start, end, desc=False, withscores=False, 1157 | score_cast_func=float): 1158 | zset = self._get_zset(name, "ZRANGE") 1159 | 1160 | if not zset: 1161 | return [] 1162 | 1163 | start, end = self._translate_range(len(zset), start, end) 1164 | 1165 | func = self._range_func(withscores, score_cast_func) 1166 | return [func(item) for item in zset.range(start, end, desc)] 1167 | 1168 | def zrangebyscore(self, name, min, max, start=None, num=None, 1169 | withscores=False, score_cast_func=float): 1170 | if (start is None) ^ (num is None): 1171 | raise RedisError('`start` and `num` must both be specified') 1172 | 1173 | zset = self._get_zset(name, "ZRANGEBYSCORE") 1174 | 1175 | if not zset: 1176 | return [] 1177 | 1178 | func = self._range_func(withscores, score_cast_func) 1179 | include_start, min = self._score_inclusive(min) 1180 | include_end, max = self._score_inclusive(max) 1181 | scorerange = zset.scorerange(min, max, start_inclusive=include_start, end_inclusive=include_end) # noqa 1182 | if start is not None and num is not None: 1183 | start, num = self._translate_limit(len(scorerange), int(start), int(num)) 1184 | scorerange = scorerange[start:start + num] 1185 | return [func(item) for item in scorerange] 1186 | 1187 | def zrank(self, name, value): 1188 | zset = self._get_zset(name, "ZRANK") 1189 | 1190 | return zset.rank(self._encode(value)) if zset else None 1191 | 1192 | def zrem(self, name, *values): 1193 | zset = self._get_zset(name, "ZREM") 1194 | 1195 | if not zset: 1196 | return 0 1197 | 1198 | count_removals = lambda value: 1 if zset.remove(self._encode(value)) else 0 1199 | removal_count = sum((count_removals(value) for value in values)) 1200 | if removal_count > 0 and len(zset) == 0: 1201 | self.delete(name) 1202 | return removal_count 1203 | 1204 | def zremrangebyrank(self, name, start, end): 1205 | zset = self._get_zset(name, "ZREMRANGEBYRANK") 1206 | 1207 | if not zset: 1208 | return 0 1209 | 1210 | start, end = self._translate_range(len(zset), start, end) 1211 | count_removals = lambda score, member: 1 if zset.remove(member) else 0 1212 | removal_count = sum((count_removals(score, member) for score, member in zset.range(start, end))) # noqa 1213 | if removal_count > 0 and len(zset) == 0: 1214 | self.delete(name) 1215 | return removal_count 1216 | 1217 | def zremrangebyscore(self, name, min, max): 1218 | zset = self._get_zset(name, "ZREMRANGEBYSCORE") 1219 | 1220 | if not zset: 1221 | return 0 1222 | 1223 | count_removals = lambda score, member: 1 if zset.remove(member) else 0 1224 | include_start, min = self._score_inclusive(min) 1225 | include_end, max = self._score_inclusive(max) 1226 | 1227 | removal_count = sum((count_removals(score, member) 1228 | for score, member in zset.scorerange(min, max, 1229 | start_inclusive=include_start, 1230 | end_inclusive=include_end))) 1231 | if removal_count > 0 and len(zset) == 0: 1232 | self.delete(name) 1233 | return removal_count 1234 | 1235 | def zrevrange(self, name, start, end, withscores=False, 1236 | score_cast_func=float): 1237 | return self.zrange(name, start, end, 1238 | desc=True, withscores=withscores, score_cast_func=score_cast_func) 1239 | 1240 | def zrevrangebyscore(self, name, max, min, start=None, num=None, 1241 | withscores=False, score_cast_func=float): 1242 | 1243 | if (start is None) ^ (num is None): 1244 | raise RedisError('`start` and `num` must both be specified') 1245 | 1246 | zset = self._get_zset(name, "ZREVRANGEBYSCORE") 1247 | if not zset: 1248 | return [] 1249 | 1250 | func = self._range_func(withscores, score_cast_func) 1251 | include_start, min = self._score_inclusive(min) 1252 | include_end, max = self._score_inclusive(max) 1253 | 1254 | scorerange = [x for x in reversed(zset.scorerange(float(min), float(max), 1255 | start_inclusive=include_start, 1256 | end_inclusive=include_end))] 1257 | if start is not None and num is not None: 1258 | start, num = self._translate_limit(len(scorerange), int(start), int(num)) 1259 | scorerange = scorerange[start:start + num] 1260 | return [func(item) for item in scorerange] 1261 | 1262 | def zrevrank(self, name, value): 1263 | zset = self._get_zset(name, "ZREVRANK") 1264 | 1265 | if zset is None: 1266 | return None 1267 | 1268 | rank = zset.rank(self._encode(value)) 1269 | if rank is None: 1270 | return None 1271 | 1272 | return len(zset) - rank - 1 1273 | 1274 | def zscore(self, name, value): 1275 | zset = self._get_zset(name, "ZSCORE") 1276 | 1277 | return zset.score(self._encode(value)) if zset is not None else None 1278 | 1279 | def zunionstore(self, dest, keys, aggregate=None): 1280 | union = SortedSet() 1281 | aggregate_func = self._aggregate_func(aggregate) 1282 | 1283 | for key in keys: 1284 | zset = self._get_zset(key, "ZUNIONSTORE") 1285 | if not zset: 1286 | continue 1287 | 1288 | for score, member in zset: 1289 | if member in union: 1290 | union[member] = aggregate_func(union[member], score) 1291 | else: 1292 | union[member] = score 1293 | 1294 | # always override existing keys 1295 | self.redis[self._encode(dest)] = union 1296 | return len(union) 1297 | 1298 | # Script Commands # 1299 | 1300 | def eval(self, script, numkeys, *keys_and_args): 1301 | """Emulate eval""" 1302 | sha = self.script_load(script) 1303 | return self.evalsha(sha, numkeys, *keys_and_args) 1304 | 1305 | def evalsha(self, sha, numkeys, *keys_and_args): 1306 | """Emulates evalsha""" 1307 | if not self.script_exists(sha)[0]: 1308 | raise RedisError("Sha not registered") 1309 | script_callable = Script(self, self.shas[sha], self.load_lua_dependencies) 1310 | numkeys = max(numkeys, 0) 1311 | keys = keys_and_args[:numkeys] 1312 | args = keys_and_args[numkeys:] 1313 | return script_callable(keys, args) 1314 | 1315 | def script_exists(self, *args): 1316 | """Emulates script_exists""" 1317 | return [arg in self.shas for arg in args] 1318 | 1319 | def script_flush(self): 1320 | """Emulate script_flush""" 1321 | self.shas.clear() 1322 | 1323 | def script_kill(self): 1324 | """Emulate script_kill""" 1325 | """XXX: To be implemented, should not be called before that.""" 1326 | raise NotImplementedError("Not yet implemented.") 1327 | 1328 | def script_load(self, script): 1329 | """Emulate script_load""" 1330 | sha_digest = sha1(script.encode("utf-8")).hexdigest() 1331 | self.shas[sha_digest] = script 1332 | return sha_digest 1333 | 1334 | def register_script(self, script): 1335 | """Emulate register_script""" 1336 | return Script(self, script, self.load_lua_dependencies) 1337 | 1338 | def call(self, command, *args): 1339 | """ 1340 | Sends call to the function, whose name is specified by command. 1341 | 1342 | Used by Script invocations and normalizes calls using standard 1343 | Redis arguments to use the expected redis-py arguments. 1344 | """ 1345 | command = self._normalize_command_name(command) 1346 | args = self._normalize_command_args(command, *args) 1347 | 1348 | redis_function = getattr(self, command) 1349 | value = redis_function(*args) 1350 | return self._normalize_command_response(command, value) 1351 | 1352 | def _normalize_command_name(self, command): 1353 | """ 1354 | Modifies the command string to match the redis client method name. 1355 | """ 1356 | command = command.lower() 1357 | 1358 | if command == 'del': 1359 | return 'delete' 1360 | 1361 | return command 1362 | 1363 | def _normalize_command_args(self, command, *args): 1364 | """ 1365 | Modifies the command arguments to match the 1366 | strictness of the redis client. 1367 | """ 1368 | if command == 'zadd' and not self.strict and len(args) >= 3: 1369 | # Reorder score and name 1370 | zadd_args = [x for tup in zip(args[2::2], args[1::2]) for x in tup] 1371 | return [args[0]] + zadd_args 1372 | 1373 | if command in ('zrangebyscore', 'zrevrangebyscore'): 1374 | # expected format is: name min max start num with_scores score_cast_func 1375 | if len(args) <= 3: 1376 | # just plain min/max 1377 | return args 1378 | 1379 | start, num = None, None 1380 | withscores = False 1381 | 1382 | for i, arg in enumerate(args[3:], 3): 1383 | # keywords are case-insensitive 1384 | lower_arg = self._encode(arg).lower() 1385 | 1386 | # handle "limit" 1387 | if lower_arg == b"limit" and i + 2 < len(args): 1388 | start, num = args[i + 1], args[i + 2] 1389 | 1390 | # handle "withscores" 1391 | if lower_arg == b"withscores": 1392 | withscores = True 1393 | 1394 | # do not expect to set score_cast_func 1395 | 1396 | return args[:3] + (start, num, withscores) 1397 | 1398 | return args 1399 | 1400 | def _normalize_command_response(self, command, response): 1401 | if command in ('zrange', 'zrevrange', 'zrangebyscore', 'zrevrangebyscore'): 1402 | if response and isinstance(response[0], tuple): 1403 | return [value for tpl in response for value in tpl] 1404 | 1405 | return response 1406 | 1407 | # Config Set/Get commands # 1408 | 1409 | def config_set(self, name, value): 1410 | """ 1411 | Set a configuration parameter. 1412 | """ 1413 | self.redis_config[name] = value 1414 | 1415 | def config_get(self, pattern='*'): 1416 | """ 1417 | Get one or more configuration parameters. 1418 | """ 1419 | result = {} 1420 | for name, value in self.redis_config.items(): 1421 | if fnmatch.fnmatch(name, pattern): 1422 | try: 1423 | result[name] = int(value) 1424 | except ValueError: 1425 | result[name] = value 1426 | return result 1427 | 1428 | # PubSub commands # 1429 | 1430 | def pubsub(self, **kwargs): 1431 | """ Return a mocked 'PubSub' object """ 1432 | if not self._pubsub: 1433 | self._pubsub = Pubsub(self, **kwargs) 1434 | return self._pubsub 1435 | 1436 | def publish(self, channel, message): 1437 | self.pubsub().publish(channel, message) 1438 | 1439 | # Internal # 1440 | 1441 | def _get_list(self, key, operation, create=False): 1442 | """ 1443 | Get (and maybe create) a list by name. 1444 | """ 1445 | return self._get_by_type(key, operation, create, b'list', []) 1446 | 1447 | def _get_set(self, key, operation, create=False): 1448 | """ 1449 | Get (and maybe create) a set by name. 1450 | """ 1451 | return self._get_by_type(key, operation, create, b'set', set()) 1452 | 1453 | def _get_hash(self, name, operation, create=False): 1454 | """ 1455 | Get (and maybe create) a hash by name. 1456 | """ 1457 | return self._get_by_type(name, operation, create, b'hash', {}) 1458 | 1459 | def _get_zset(self, name, operation, create=False): 1460 | """ 1461 | Get (and maybe create) a sorted set by name. 1462 | """ 1463 | return self._get_by_type(name, operation, create, b'zset', SortedSet(), return_default=False) # noqa 1464 | 1465 | def _get_by_type(self, key, operation, create, type_, default, return_default=True): 1466 | """ 1467 | Get (and maybe create) a redis data structure by name and type. 1468 | """ 1469 | key = self._encode(key) 1470 | if self.type(key) in [type_, b'none']: 1471 | if create: 1472 | return self.redis.setdefault(key, default) 1473 | else: 1474 | return self.redis.get(key, default if return_default else None) 1475 | 1476 | raise TypeError("{} requires a {}".format(operation, type_)) 1477 | 1478 | def _translate_range(self, len_, start, end): 1479 | """ 1480 | Translate range to valid bounds. 1481 | """ 1482 | if start < 0: 1483 | start += len_ 1484 | start = max(0, min(start, len_)) 1485 | if end < 0: 1486 | end += len_ 1487 | end = max(-1, min(end, len_ - 1)) 1488 | return start, end 1489 | 1490 | def _translate_limit(self, len_, start, num): 1491 | """ 1492 | Translate limit to valid bounds. 1493 | """ 1494 | if start > len_ or num <= 0: 1495 | return 0, 0 1496 | return min(start, len_), num 1497 | 1498 | def _range_func(self, withscores, score_cast_func): 1499 | """ 1500 | Return a suitable function from (score, member) 1501 | """ 1502 | if withscores: 1503 | return lambda score_member: (score_member[1], score_cast_func(self._encode(score_member[0]))) # noqa 1504 | else: 1505 | return lambda score_member: score_member[1] 1506 | 1507 | def _aggregate_func(self, aggregate): 1508 | """ 1509 | Return a suitable aggregate score function. 1510 | """ 1511 | funcs = {"sum": add, "min": min, "max": max} 1512 | func_name = aggregate.lower() if aggregate else 'sum' 1513 | try: 1514 | return funcs[func_name] 1515 | except KeyError: 1516 | raise TypeError("Unsupported aggregate: {}".format(aggregate)) 1517 | 1518 | def _apply_to_sets(self, func, operation, keys, *args): 1519 | """Helper function for sdiff, sinter, and sunion""" 1520 | keys = self._list_or_args(keys, args) 1521 | if not keys: 1522 | raise TypeError("{} takes at least two arguments".format(operation.lower())) 1523 | left = self._get_set(keys[0], operation) or set() 1524 | for key in keys[1:]: 1525 | right = self._get_set(key, operation) or set() 1526 | left = func(left, right) 1527 | return left 1528 | 1529 | def _list_or_args(self, keys, args): 1530 | """ 1531 | Shamelessly copied from redis-py. 1532 | """ 1533 | # returns a single list combining keys and args 1534 | try: 1535 | iter(keys) 1536 | # a string can be iterated, but indicates 1537 | # keys wasn't passed as a list 1538 | if isinstance(keys, basestring): 1539 | keys = [keys] 1540 | except TypeError: 1541 | keys = [keys] 1542 | if args: 1543 | keys.extend(args) 1544 | return keys 1545 | 1546 | def _score_inclusive(self, score): 1547 | if isinstance(score, basestring) and score[0] == '(': 1548 | return False, float(score[1:]) 1549 | return True, float(score) 1550 | 1551 | def _encode(self, value): 1552 | "Return a bytestring representation of the value. Taken from redis-py connection.py" 1553 | if isinstance(value, bytes): 1554 | return value 1555 | elif isinstance(value, (int, long)): 1556 | value = str(value).encode('utf-8') 1557 | elif isinstance(value, float): 1558 | value = repr(value).encode('utf-8') 1559 | elif not isinstance(value, basestring): 1560 | value = str(value).encode('utf-8') 1561 | else: 1562 | value = value.encode('utf-8', 'strict') 1563 | return value 1564 | 1565 | 1566 | def get_total_milliseconds(td): 1567 | return int((td.days * 24 * 60 * 60 + td.seconds) * 1000 + td.microseconds / 1000.0) 1568 | 1569 | 1570 | def mock_redis_client(**kwargs): 1571 | """ 1572 | Mock common.util.redis_client so we 1573 | can return a MockRedis object 1574 | instead of a Redis object. 1575 | """ 1576 | return MockRedis() 1577 | 1578 | mock_redis_client.from_url = mock_redis_client 1579 | 1580 | 1581 | def mock_strict_redis_client(**kwargs): 1582 | """ 1583 | Mock common.util.redis_client so we 1584 | can return a MockRedis object 1585 | instead of a StrictRedis object. 1586 | """ 1587 | return MockRedis(strict=True) 1588 | 1589 | mock_strict_redis_client.from_url = mock_strict_redis_client 1590 | --------------------------------------------------------------------------------