diff options
-rw-r--r-- | README.md | 65 | ||||
-rw-r--r-- | redis/__init__.py | 5 | ||||
-rw-r--r-- | redis/client.py | 109 | ||||
-rw-r--r-- | tests/server_commands.py | 35 |
4 files changed, 139 insertions, 75 deletions
@@ -18,12 +18,46 @@ From source: ## Getting Started >>> import redis - >>> r = redis.Redis(host='localhost', port=6379, db=0) + >>> r = redis.StrictRedis(host='localhost', port=6379, db=0) >>> r.set('foo', 'bar') True >>> r.get('foo') 'bar' +## API Reference + +The official Redis documentation does a great job of explaining each command in +detail (http://redis.io/commands). redis-py exposes two client classes that +implement these commands. The StrictRedis class attempts to adhere to the +official official command syntax. There are a few exceptions: + +* SELECT: Not implemented. See the explanation in the Thread Safety section + below. +* DEL: 'del' is a reserved keyword in the Python syntax. Therefore redis-py + uses 'delete' instead. +* CONFIG GET|SET: These are implemented separately as config_get or config_set. +* MULTI/EXEC: These are implemented as part of the Pipeline class. Calling + the pipeline method and specifying use_transaction=True will cause the + pipeline to be wrapped with the MULTI and EXEC statements when it is executed. + See more about Pipelines below. +* SUBSCRIBE/LISTEN: Similar to pipelines, PubSub is implemented as a separate + class as it places the underlying connection in a state where it can't + execute non-pubsub commands. Calling the pubsub method from the Redis client + will return a PubSub instance where you can subscribe to channels and listen + for messages. You can call PUBLISH from both classes. + +In addition to the changes above, the Redis class, a subclass of StrictRedis, +overrides several other commands to provide backwards compatibility with older +versions of redis-py: + +* LREM: Order of 'num' and 'value' arguments reversed such that 'num' can + provide a default value of zero. +* ZADD: Redis specifies the 'score' argument before 'value'. These were swapped + accidentally when being implemented and not discovered until after people + were already using it. The Redis class expects *args in the form of: + name1, score1, name2, score2, ... + + ## More Detail ### Connection Pools @@ -232,35 +266,6 @@ which is much easier to read: >>> r.transaction(client_side_incr, 'OUR-SEQUENCE-KEY') [True] - -## API Reference - -The official Redis documentation does a great job of explaining each command in -detail (http://redis.io/commands). In most cases, redis-py uses the same -arguments as the official spec. There are a few exceptions noted here: - -* SELECT: Not implemented. See the explanation in the Thread Safety section - above. -* ZADD: Redis specifies the 'score' argument before 'value'. These were swapped - accidentally when being implemented and not discovered until after people - were already using it. As of Redis 2.4, ZADD will start supporting variable - arguments. redis-py implements these as python keyword arguments where the - name is the 'value' and the value is the 'score'. -* DEL: 'del' is a reserved keyword in the Python syntax. Therefore redis-py - uses 'delete' instead. -* CONFIG GET|SET: These are implemented separately as config_get or config_set. -* MULTI/EXEC: These are implemented as part of the Pipeline class. Calling - the pipeline method and specifying use_transaction=True will cause the - pipeline to be wrapped with the MULTI and EXEC statements when it is executed. - See more about Pipelines above. -* SUBSCRIBE/LISTEN: Similar to pipelines, PubSub is implemented as a separate - class as it places the underlying connection in a state where it can't - execute non-pubsub commands. Calling the pubsub method from the Redis client - will return a PubSub instance where you can subscribe to channels and listen - for messages. You can call PUBLISH from both classes. -* LREM: Order of 'num' and 'value' arguments reversed such that 'num' can - provide a default value of zero. - ## Versioning scheme redis-py is versioned after Redis. For example, redis-py 2.0.0 should diff --git a/redis/__init__.py b/redis/__init__.py index 48b7ddd..8f94044 100644 --- a/redis/__init__.py +++ b/redis/__init__.py @@ -1,4 +1,4 @@ -from redis.client import Redis +from redis.client import Redis, StrictRedis from redis.connection import ( ConnectionPool, Connection, @@ -20,7 +20,8 @@ __version__ = '2.4.9' VERSION = tuple(map(int, __version__.split('.'))) __all__ = [ - 'Redis', 'ConnectionPool', 'Connection', 'UnixDomainSocketConnection', + 'Redis', 'StrictRedis', 'ConnectionPool', + 'Connection', 'UnixDomainSocketConnection', 'RedisError', 'ConnectionError', 'ResponseError', 'AuthenticationError', 'InvalidResponse', 'DataError', 'PubSubError', 'WatchError', ] diff --git a/redis/client.py b/redis/client.py index 24f8d81..7412d62 100644 --- a/redis/client.py +++ b/redis/client.py @@ -102,7 +102,7 @@ def parse_config(response, **options): return response and pairs_to_dict(response) or {} return response == 'OK' -class Redis(object): +class StrictRedis(object): """ Implementation of the Redis protocol. @@ -628,13 +628,17 @@ class Redis(object): """ return self.execute_command('LRANGE', name, start, end) - def lrem(self, name, value, num=0): + def lrem(self, name, count, value): """ - Remove the first ``num`` occurrences of ``value`` from list ``name`` + Remove the first ``count`` occurrences of elements equal to ``value`` + from the list stored at ``name``. - If ``num`` is 0, then all occurrences will be removed + The count argument influences the operation in the following ways: + count > 0: Remove elements equal to value moving from head to tail. + count < 0: Remove elements equal to value moving from tail to head. + count = 0: Remove all elements equal to value. """ - return self.execute_command('LREM', name, num, value) + return self.execute_command('LREM', name, count, value) def lset(self, name, index, value): "Set ``position`` of list ``name`` to ``value``" @@ -798,27 +802,27 @@ class Redis(object): #### SORTED SET COMMANDS #### - def zadd(self, name, value=None, score=None, **pairs): - """ - For each kwarg in ``pairs``, add that item and it's score to the - sorted set ``name``. - - The ``value`` and ``score`` arguments are deprecated. - """ - all_pairs = [] - if value is not None or score is not None: - if value is None or score is None: - raise RedisError("Both 'value' and 'score' must be specified " \ - "to ZADD") - warnings.warn(DeprecationWarning( - "Passing 'value' and 'score' has been deprecated. " \ - "Please pass via kwargs instead.")) - all_pairs.append(score) - all_pairs.append(value) - for pair in pairs.iteritems(): - all_pairs.append(pair[1]) - all_pairs.append(pair[0]) - return self.execute_command('ZADD', name, *all_pairs) + def zadd(self, name, *args, **kwargs): + """ + Set any number of score, element-name pairs to the key ``name``. Pairs + can be specified in two ways: + + As *args, in the form of: score1, name1, score2, name2, ... + or as **kwargs, in the form of: name1=score1, name2=score2, ... + + The following example would add four values to the 'my-key' key: + redis.zadd('my-key', 1.1, 'name1', 2.2, 'name2', name3=3.3, name4=4.4) + """ + pieces = [] + if args: + if len(args) % 2 != 0: + raise RedisError("ZADD requires an equal number of " + "values and scores") + pieces.extend(args) + for pair in kwargs.iteritems(): + pieces.append(pair[1]) + pieces.append(pair[0]) + return self.execute_command('ZADD', name, *pieces) def zcard(self, name): "Return the number of elements in the sorted set ``name``" @@ -1063,6 +1067,59 @@ class Redis(object): return self.execute_command('PUBLISH', channel, message) +class Redis(StrictRedis): + """ + Provides backwards compatibility with older versions of redis-py that + changed arguemnts to some commands to be more Pythonic, sane, or + accident. + """ + + def lrem(self, name, value, num=0): + """ + Remove the first ``num`` occurrences of elements equal to ``value`` + from the list stored at ``name``. + + The ``num`` argument influences the operation in the following ways: + num > 0: Remove elements equal to value moving from head to tail. + num < 0: Remove elements equal to value moving from tail to head. + num = 0: Remove all elements equal to value. + """ + return self.execute_command('LREM', name, num, value) + + def zadd(self, name, *args, **kwargs): + """ + NOTE: The order of arguments differs from that of the official ZADD + command. For backwards compatability, this method accepts arguments + in the form of name1, score1, name2, score2, while the official Redis + documents expects score1, name1, score2, name2. + + If you're looking to use the standard syntax, consider using the + StrictRedis class. See the API Reference section of the docs for more + information. + + Set any number of element-name, score pairs to the key ``name``. Pairs + can be specified in two ways: + + As *args, in the form of: name1, score1, name2, score2, ... + or as **kwargs, in the form of: name1=score1, name2=score2, ... + + The following example would add four values to the 'my-key' key: + redis.zadd('my-key', 'name1', 1.1, 'name2', 2.2, name3=3.3, name4=4.4) + """ + pieces = [] + if args: + if len(args) % 2 != 0: + raise RedisError("ZADD requires an equal number of " + "values and scores") + temp_args = args + temp_args.reverse() + pieces.extend(temp_args) + for pair in kwargs.iteritems(): + pieces.append(pair[1]) + pieces.append(pair[0]) + return self.execute_command('ZADD', name, *pieces) + + class PubSub(object): """ PubSub provides publish, subscribe and listen support to Redis channels. diff --git a/tests/server_commands.py b/tests/server_commands.py index 9e3375d..e1f90d3 100644 --- a/tests/server_commands.py +++ b/tests/server_commands.py @@ -7,8 +7,8 @@ from redis.client import parse_info class ServerCommandsTestCase(unittest.TestCase): - def get_client(self): - return redis.Redis(host='localhost', port=6379, db=9) + def get_client(self, cls=redis.Redis): + return cls(host='localhost', port=6379, db=9) def setUp(self): self.client = self.get_client() @@ -438,20 +438,6 @@ class ServerCommandsTestCase(unittest.TestCase): self.assert_(self.client.ltrim('a', 0, 1)) self.assertEquals(self.client.lrange('a', 0, 5), ['a', 'b']) - def test_lpop(self): - # no key - self.assertEquals(self.client.lpop('a'), None) - # key is not a list - self.client['a'] = 'b' - self.assertRaises(redis.ResponseError, self.client.lpop, 'a') - del self.client['a'] - # real logic - self.make_list('a', 'abc') - self.assertEquals(self.client.lpop('a'), 'a') - self.assertEquals(self.client.lpop('a'), 'b') - self.assertEquals(self.client.lpop('a'), 'c') - self.assertEquals(self.client.lpop('a'), None) - def test_rpop(self): # no key self.assertEquals(self.client.rpop('a'), None) @@ -875,7 +861,7 @@ class ServerCommandsTestCase(unittest.TestCase): # a non existant key should return empty list self.assertEquals(self.client.zrange('b', 0, 1, withscores=True), []) - def test_zrangebyscore(self): + def test_zrevrangebyscore(self): # key is not a zset self.client['a'] = 'a' self.assertRaises(redis.ResponseError, self.client.zrevrangebyscore, @@ -1193,6 +1179,21 @@ class ServerCommandsTestCase(unittest.TestCase): self.assertEquals(self.client.lrange('sorted', 0, 10), ['vodka', 'milk', 'gin', 'apple juice']) + def test_strict_zadd(self): + client = self.get_client(redis.StrictRedis) + client.zadd('a', 1.0, 'a1', 2.0, 'a2', a3=3.0) + self.assertEquals(client.zrange('a', 0, 3, withscores=True), + [('a1', 1.0), ('a2', 2.0), ('a3', 3.0)]) + + def test_strict_lrem(self): + client = self.get_client(redis.StrictRedis) + client.rpush('a', 'a1') + client.rpush('a', 'a2') + client.rpush('a', 'a3') + client.rpush('a', 'a1') + client.lrem('a', 0, 'a1') + self.assertEquals(client.lrange('a', 0, -1), ['a2', 'a3']) + ## BINARY SAFE # TODO add more tests def test_binary_get_set(self): |