diff --git a/.travis.yml b/.travis.yml index b999356..e21cb75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,13 @@ python: - '3.4' - pypy - pypy3 -install: pip install . --use-mirrors +install: pip install . script: python setup.py nosetests deploy: provider: pypi - user: jessemyers + user: github-ll password: - secure: f7wqGCuSP6dJZ7GwdpQwMc9J0GU8oCK8tydUnvgLNass0U57Eg+PaQURPWxg0T1f5nYACzfche3YHTFeXtk1x7HV6WYWYddin/gv94m7u9+ZQpsrygGizv7eO3lU/vR3K86GrLdTR3z7ii50XDlM0W8XyqHDX1+GGHPWFGLxOHo= + secure: WZNNslmr6ELiasA6IhO9QDQ/g7758WjezVLqC3Ebo3EgAsUOFeCvtqODf3U8czz4UxORrVL7xWN/lUEOCG/ImDoXa9W27h8kl3D0pP9s8FDNngZspB5c3xhZhQesiqB6n8Fyi2dLic9QYUUIgquo2w+w/r5rRHvplI9OVbIGuVM= on: tags: true repo: locationlabs/mockredis diff --git a/CHANGES.md b/CHANGES.md index 20de6a0..4032f76 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,17 @@ +Version 2.9.3 + - Support for `from_url` + - Going to remove develop and use master following github flow model. + +Version 2.9.2 + - Fixed the versioning issue. + +Version 2.9.1 + - Support for `transaction` + - Fix `do_expire` method in Python 3 + +Version 2.9.0.12 + - Support: `dbsize` + Version 2.9.0.11 - Support: `scan_iter`, `sscan_iter`, `zscan_iter`, `hscan_iter` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ad86b2..3c63fb5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,28 +1,16 @@ # Contributing -Mock Redis uses [git-flow][1] for its branch management. +Mock Redis uses GitHub Flow for its branch management. We ask that contributors to this project please: - 1. Implement changes in new git branches, following git-flow's model: + 1. Folow [GitHub Flow][1]. - - Changes based off of *develop* will receive the least amount of skepticism. - - - Changes based off of a *release* branches (if one exists) will be considered, - especially for small bug fixes relevant to the release. We are not likely to - accept new features against *release* branches. - - - Changes based off of *master* or a prior release tag will be given the most - skepticism. We may accept patches for major bugs against past releases, but - would prefer to see such changes follow the normal git-flow process. - - We will not accept new features based off of *master*. - 2. Limit the scope of changes to a single bug fix or feature per branch. 3. Treat documentation and unit tests as an essential part of any change. - 4. Update the change log appropriately. + 4. Follow existing style, contributions should look as part of the project. Thank you! - [1]: https://github.com/nvie/gitflow + [1]: https://guides.github.com/introduction/flow/ diff --git a/README.md b/README.md index a4a5a41..2b4f653 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +THIS REPO IS NO LONGER SUPPORTED. IF YOU ARE LOOKING FOR A SIMILAR SOLUTION, WE WOULD RECOMMEND LOOKING AT https://github.com/jamesls/fakeredis + # Mock for the redis-py client library Supports writing tests for code using the [redis-py][redis-py] library diff --git a/mockredis/client.py b/mockredis/client.py index 998d3c5..5de0b2a 100644 --- a/mockredis/client.py +++ b/mockredis/client.py @@ -1,5 +1,6 @@ from __future__ import division from collections import defaultdict +from copy import deepcopy from itertools import chain from datetime import datetime, timedelta from hashlib import sha1 @@ -8,10 +9,11 @@ import re import sys import time +import fnmatch from mockredis.clock import SystemClock from mockredis.lock import MockRedisLock -from mockredis.exceptions import RedisError, ResponseError +from mockredis.exceptions import RedisError, ResponseError, WatchError from mockredis.pipeline import MockRedisPipeline from mockredis.script import Script from mockredis.sortedset import SortedSet @@ -51,12 +53,17 @@ def __init__(self, self.blocking_sleep_interval = blocking_sleep_interval # The 'Redis' store self.redis = defaultdict(dict) + self.redis_config = defaultdict(dict) self.timeouts = defaultdict(dict) # The 'PubSub' store self.pubsub = defaultdict(list) # Dictionary from script to sha ''Script'' self.shas = dict() + @classmethod + def from_url(http://github.com/cls,%20url,%20db=None,%20**kwargs): + return cls(**kwargs) + # Connection Functions # def echo(self, msg): @@ -75,6 +82,30 @@ def pipeline(self, transaction=True, shard_hint=None): """Emulate a redis-python pipeline.""" return MockRedisPipeline(self, transaction, shard_hint) + def transaction(self, func, *watches, **kwargs): + """ + Convenience method for executing the callable `func` as a transaction + while watching all keys specified in `watches`. The 'func' callable + should expect a single argument which is a Pipeline object. + + Copied directly from redis-py. + """ + shard_hint = kwargs.pop('shard_hint', None) + value_from_callable = kwargs.pop('value_from_callable', False) + watch_delay = kwargs.pop('watch_delay', None) + with self.pipeline(True, shard_hint) as pipe: + while 1: + try: + if watches: + pipe.watch(*watches) + func_value = func(pipe) + exec_value = pipe.execute() + return func_value if value_from_callable else exec_value + except WatchError: + if watch_delay is not None and watch_delay > 0: + time.sleep(watch_delay) + continue + def watch(self, *argv, **kwargs): """ Mock does not support command buffering so watch @@ -122,13 +153,20 @@ def type(self, key): def keys(self, pattern='*'): """Emulate keys.""" - # Make a regex out of pattern. The only special matching character we look for is '*' - regex = re.compile(b'^' + re.escape(self._encode(pattern)).replace(b'\\*', b'.*') + b'$') + # making sure the pattern is unicode/str. + try: + pattern = pattern.decode('utf-8') + # This throws an AttributeError in python 3, or an + # UnicodeEncodeError in python 2 + except (AttributeError, UnicodeEncodeError): + pass - # Find every key that matches the pattern - result = [key for key in self.redis.keys() if regex.match(key)] + # Make regex out of glob styled pattern. + regex = fnmatch.translate(pattern) + regex = re.compile(re.sub(r'(^|[^\\])\.', r'\1[^/]', regex)) - return result + # Find every key that matches the pattern + return [key for key in self.redis.keys() if regex.match(key.decode('utf-8'))] def delete(self, *keys): """Emulate delete.""" @@ -221,7 +259,9 @@ def do_expire(self): """ Expire objects assuming now == time """ - for key, value in self.timeouts.items(): + # Deep copy to avoid RuntimeError: dictionary changed size during iteration + _timeouts = deepcopy(self.timeouts) + for key, value in _timeouts.items(): if value - self.clock.now() < timedelta(0): del self.timeouts[key] # removing the expired key @@ -247,6 +287,9 @@ def _rename(self, old_key, new_key, nx=False): return True return False + def dbsize(self): + return len(self.redis.keys()) + # String Functions # def get(self, key): @@ -337,16 +380,16 @@ def _should_set(self, key, mode): # for all other cases, return true return True - def setex(self, key, time, value): + def setex(self, name, time, value): """ - Set the value of ``key`` to ``value`` that expires in ``time`` + Set the value of ``name`` to ``value`` that expires in ``time`` seconds. ``time`` can be represented by an integer or a Python timedelta object. """ if not self.strict: # when not strict mode swap value and time args order time, value = value, time - return self.set(key, value, ex=time) + return self.set(name, value, ex=time) def psetex(self, key, time, value): """ @@ -365,12 +408,15 @@ def mset(self, *args, **kwargs): Sets key/values based on a mapping. Mapping can be supplied as a single dictionary argument or as kwargs. """ + mapping = kwargs if args: if len(args) != 1 or not isinstance(args[0], dict): raise RedisError('MSET requires **kwargs or a single dict arg') - mapping = args[0] - else: - mapping = kwargs + mapping.update(args[0]) + + if len(mapping) == 0: + raise ResponseError("wrong number of arguments for 'mset' command") + for key, value in mapping.items(): self.set(key, value) return True @@ -388,6 +434,9 @@ def msetnx(self, *args, **kwargs): else: mapping = kwargs + if len(mapping) == 0: + raise ResponseError("wrong number of arguments for 'msetnx' command") + for key in mapping.keys(): if self._encode(key) in self.redis: return False @@ -648,7 +697,11 @@ def lpush(self, key, *args): # Creates the list at this key if it doesn't exist, and appends args to its beginning args_reversed = [self._encode(arg) for arg in args] args_reversed.reverse() - self.redis[self._encode(key)] = args_reversed + redis_list + updated_list = args_reversed + redis_list + self.redis[self._encode(key)] = updated_list + + # Return the length of the list after the push operation + return len(updated_list) def rpop(self, key): """Emulate lpop.""" @@ -673,6 +726,9 @@ def rpush(self, key, *args): # Creates the list at this key if it doesn't exist, and appends args to it redis_list.extend(map(self._encode, args)) + # Return the length of the list after the push operation + return len(redis_list) + def lrem(self, key, value, count=0): """Emulate lrem.""" value = self._encode(value) @@ -1354,6 +1410,27 @@ def _normalize_command_response(self, command, response): return response + # Config Set/Get commands # + + def config_set(self, name, value): + """ + Set a configuration parameter. + """ + self.redis_config[name] = value + + def config_get(self, pattern='*'): + """ + Get one or more configuration parameters. + """ + result = {} + for name, value in self.redis_config.items(): + if fnmatch.fnmatch(name, pattern): + try: + result[name] = int(value) + except ValueError: + result[name] = value + return result + # PubSub commands # def publish(self, channel, message): @@ -1498,6 +1575,8 @@ def mock_redis_client(**kwargs): """ return MockRedis() +mock_redis_client.from_url = mock_redis_client + def mock_strict_redis_client(**kwargs): """ @@ -1506,3 +1585,5 @@ def mock_strict_redis_client(**kwargs): instead of a StrictRedis object. """ return MockRedis(strict=True) + +mock_strict_redis_client.from_url = mock_strict_redis_client diff --git a/mockredis/tests/test_config.py b/mockredis/tests/test_config.py new file mode 100644 index 0000000..24a7f94 --- /dev/null +++ b/mockredis/tests/test_config.py @@ -0,0 +1,20 @@ +from nose.tools import eq_, ok_ + +from mockredis.tests.fixtures import setup, teardown + + +class TestRedisConfig(object): + """Redis config set/get tests""" + + def setup(self): + setup(self) + + def teardown(self): + teardown(self) + + def test_config_set(self): + eq_(self.redis.config_get('config-param'), {}) + self.redis.config_set('config-param', 'value') + eq_(self.redis.config_get('config-param'), {'config-param': 'value'}) + eq_(self.redis.config_get('config*'), {'config-param': 'value'}) + diff --git a/mockredis/tests/test_factories.py b/mockredis/tests/test_factories.py index 103b3f5..0d2084d 100644 --- a/mockredis/tests/test_factories.py +++ b/mockredis/tests/test_factories.py @@ -13,8 +13,22 @@ def test_mock_redis_client(): ok_(not mock_redis_client(host="localhost", port=6379).strict) +def test_mock_redis_client_from_url(): + """ + Test that we can pass kwargs to the Redis from_url mock/patch target. + """ + ok_(not mock_redis_client.from_url(http://github.com/host=%22localhost%22,%20port=6379).strict) + + def test_mock_strict_redis_client(): """ Test that we can pass kwargs to the StrictRedis mock/patch target. """ ok_(mock_strict_redis_client(host="localhost", port=6379).strict) + + +def test_mock_strict_redis_client_from_url(): + """ + Test that we can pass kwargs to the StrictRedis from_url mock/patch target. + """ + ok_(mock_strict_redis_client.from_url(http://github.com/host=%22localhost%22,%20port=6379).strict) diff --git a/mockredis/tests/test_list.py b/mockredis/tests/test_list.py index 5269d10..d9b85be 100644 --- a/mockredis/tests/test_list.py +++ b/mockredis/tests/test_list.py @@ -86,15 +86,15 @@ def test_lpush(self): Insertion maintains order but not uniqueness. """ # lpush two values - self.redis.lpush(LIST1, VAL1) - self.redis.lpush(LIST1, VAL2) + eq_(1, self.redis.lpush(LIST1, VAL1)) + eq_(2, self.redis.lpush(LIST1, VAL2)) # validate insertion eq_(b"list", self.redis.type(LIST1)) eq_([bVAL2, bVAL1], self.redis.lrange(LIST1, 0, -1)) # insert two more values with one repeated - self.redis.lpush(LIST1, VAL1, VAL3) + eq_(4, self.redis.lpush(LIST1, VAL1, VAL3)) # validate the update eq_(b"list", self.redis.type(LIST1)) @@ -128,15 +128,15 @@ def test_rpush(self): Insertion maintains order but not uniqueness. """ # rpush two values - self.redis.rpush(LIST1, VAL1) - self.redis.rpush(LIST1, VAL2) + eq_(1, self.redis.rpush(LIST1, VAL1)) + eq_(2, self.redis.rpush(LIST1, VAL2)) # validate insertion eq_(b"list", self.redis.type(LIST1)) eq_([bVAL1, bVAL2], self.redis.lrange(LIST1, 0, -1)) # insert two more values with one repeated - self.redis.rpush(LIST1, VAL1, VAL3) + eq_(4, self.redis.rpush(LIST1, VAL1, VAL3)) # validate the update eq_(b"list", self.redis.type(LIST1)) diff --git a/mockredis/tests/test_pipeline.py b/mockredis/tests/test_pipeline.py index 47eb00d..73e2d51 100644 --- a/mockredis/tests/test_pipeline.py +++ b/mockredis/tests/test_pipeline.py @@ -33,6 +33,30 @@ def test_pipeline_args(self): with self.redis.pipeline(transaction=False, shard_hint=None): pass + def test_transaction(self): + self.redis["a"] = 1 + self.redis["b"] = 2 + has_run = [] + + def my_transaction(pipe): + a_value = pipe.get("a") + assert a_value in (b"1", b"2") + b_value = pipe.get("b") + assert b_value == b"2" + + # silly run-once code... incr's "a" so WatchError should be raised + # forcing this all to run again. this should incr "a" once to "2" + if not has_run: + self.redis.incr("a") + has_run.append(True) + + pipe.multi() + pipe.set("c", int(a_value) + int(b_value)) + + result = self.redis.transaction(my_transaction, "a", "b") + eq_([True], result) + eq_(b"4", self.redis["c"]) + def test_set_and_get(self): """ Pipeline execution returns the pipeline, not the intermediate value. diff --git a/mockredis/tests/test_redis.py b/mockredis/tests/test_redis.py index f502db7..2dd1b8a 100644 --- a/mockredis/tests/test_redis.py +++ b/mockredis/tests/test_redis.py @@ -199,6 +199,7 @@ def test_keys_unicode(self): self.redis.set(key, "bar") eq_([key_as_utf8], self.redis.keys("*")) eq_([key_as_utf8], self.redis.keys("eat*")) + eq_([key_as_utf8], self.redis.keys("[ea]at * n?[a-z]")) unicode_prefix = b'eat \xf0\x9f\x8d\xb0*'.decode('utf-8') eq_([key_as_utf8], self.redis.keys(unicode_prefix)) @@ -245,3 +246,9 @@ def test_renamenx(self): eq_(b"bar2", self.redis.get("foo2")) eq_(self.redis.renamenx("foo", "foo3"), 1) eq_(b"bar", self.redis.get("foo3")) + + def test_dbsize(self): + self.redis["foo"] = "bar" + eq_(1, self.redis.dbsize()) + del self.redis["foo"] + eq_(0, self.redis.dbsize()) diff --git a/mockredis/tests/test_string.py b/mockredis/tests/test_string.py index 4934921..eaa99ca 100644 --- a/mockredis/tests/test_string.py +++ b/mockredis/tests/test_string.py @@ -3,7 +3,11 @@ from nose.tools import eq_, ok_ from mockredis.client import get_total_milliseconds -from mockredis.tests.fixtures import raises_response_error, setup, teardown +from mockredis.tests.fixtures import ( + raises_response_error, + setup, + teardown, +) class TestRedisString(object): @@ -203,13 +207,31 @@ def test_strict_setex_zero_expiration(self): def test_mset(self): ok_(self.redis.mset({"key1": "hello", "key2": ""})) ok_(self.redis.mset(**{"key3": "world", "key2": "there"})) - eq_([b"hello", b"there", b"world"], self.redis.mget("key1", "key2", "key3")) + ok_(self.redis.mset( + {"key4": "ican"}, **{"key4": "haz", "key5": "cheezburger"}) + ) + eq_( + [b"hello", b"there", b"world", b"ican", b"cheezburger"], + self.redis.mget("key1", "key2", "key3", "key4", "key5") + ) + + @raises_response_error + def test_mset_empty_kwargs(self): + self.redis.mset(**{}) + + @raises_response_error + def test_mset_empty_args(self): + self.redis.mset({}) def test_msetnx(self): ok_(self.redis.msetnx({"key1": "hello", "key2": "there"})) ok_(not self.redis.msetnx(**{"key3": "world", "key2": "there"})) eq_([b"hello", b"there", None], self.redis.mget("key1", "key2", "key3")) + @raises_response_error + def test_msetnx_empty_args(self): + self.redis.msetnx({}) + def test_psetex(self): test_cases = [200, timedelta(milliseconds=250)] for case in test_cases: diff --git a/setup.py b/setup.py index db7d98c..aa59785 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages # Match releases to redis-py versions -__version__ = '2.9.0.11' +__version__ = '2.9.3' # Jenkins will replace __build__ with a unique value. __build__ = ''