summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--CHANGES2
-rw-r--r--redis/__init__.py10
-rw-r--r--redis/client.py885
-rw-r--r--redis/exceptions.py25
-rw-r--r--setup.py27
-rw-r--r--tests.py4
-rw-r--r--tests/__init__.py7
-rw-r--r--tests/server_commands.py637
9 files changed, 1567 insertions, 32 deletions
diff --git a/.gitignore b/.gitignore
index 2f78cf5..56b2976 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
*.pyc
-
+redis.egg-info
diff --git a/CHANGES b/CHANGES
index 636901b..62b2db4 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,5 @@
+* 0.7
+ * Completely refactored client
* 0.6.1
* Added support for ZINCRBY via the `zincr` command
* Swapped score and member parameters to zadd to make it more similar to other commands.
diff --git a/redis/__init__.py b/redis/__init__.py
index 54baa0a..a3eb7c0 100644
--- a/redis/__init__.py
+++ b/redis/__init__.py
@@ -1,10 +1,10 @@
# legacy imports
-from redis.old_client import Redis
-from redis.exceptions import RedisError, ConnectionError
+from redis.client import Redis
+from redis.exceptions import RedisError, ConnectionError, AuthenticationError
from redis.exceptions import ResponseError, InvalidResponse, InvalidData
__all__ = [
- 'Redis', # legacy
- 'RedisError', 'ConnectionError', 'ResponseError', # exceptions
- 'InvalidResponse', 'InvalidData', # exceptions
+ 'Redis'
+ 'RedisError', 'ConnectionError', 'ResponseError', 'AuthenticationError'
+ 'InvalidResponse', 'InvalidData',
]
diff --git a/redis/client.py b/redis/client.py
new file mode 100644
index 0000000..83c4f32
--- /dev/null
+++ b/redis/client.py
@@ -0,0 +1,885 @@
+import datetime
+import errno
+import socket
+import threading
+import warnings
+from redis.exceptions import ConnectionError, ResponseError, InvalidResponse
+from redis.exceptions import RedisError, AuthenticationError
+
+_connection_manager = threading.local()
+
+class Connection(object):
+ "Manages TCP communication to and from a Redis server"
+ def __init__(self, host='localhost', port=6379, db=0):
+ self.host = host
+ self.port = port
+ self.db = db
+ self._sock = None
+ self._fp = None
+
+ def connect(self):
+ "Connects to the Redis server is not already connected"
+ if self._sock:
+ return
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((self.host, self.port))
+ except socket.error, e:
+ raise ConnectionError("Error %s connecting to %s:%s. %s." % \
+ (e.args[0], self.host, self.port, e.args[1]))
+ sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
+ self._sock = sock
+ self._fp = sock.makefile('r')
+
+ def disconnect(self):
+ "Disconnects from the Redis server"
+ if self._sock is None:
+ return
+ try:
+ self._sock.close()
+ except socket.error:
+ pass
+ self._sock = None
+ self._fp = None
+
+ def send(self, command):
+ "Send ``command`` to the Redis server. Return the result."
+ self.connect()
+ try:
+ self._sock.sendall(command)
+ except socket.error, e:
+ if e.args[0] == errno.EPIPE:
+ self.disconnect()
+ raise ConnectionError("Error %s while writing to socket. %s." % \
+ e.args)
+
+ def read(self, length=None):
+ """
+ Read a line from the socket is length is None,
+ otherwise read ``length`` bytes
+ """
+ try:
+ if length is not None:
+ return self._fp.read(length)
+ return self._fp.readline()
+ except socket.error, e:
+ self.disconnect()
+ if e.args and e.args[0] == errno.EAGAIN:
+ raise ConnectionError("Error while reading from socket: %s" % \
+ e.args[1])
+ return ''
+
+def list_or_args(command, keys, args):
+ # returns a single list combining keys and args
+ # if keys is not a list or args has items, issue a
+ # deprecation wanring
+ oldapi = bool(args)
+ try:
+ i = iter(keys)
+ # a string can be iterated, but indicates
+ # keys wasn't passed as a list
+ if isinstance(keys, basestring):
+ oldapi = True
+ except TypeError:
+ oldapi = True
+ keys = [keys]
+ if oldapi:
+ warnings.warn(DeprecationWarning(
+ "Passing *args to Redis.%s has been deprecated. "
+ "Pass an iterable to ``keys`` instead" % command
+ ))
+ keys.extend(args)
+ return keys
+
+def timestamp_to_datetime(response):
+ "Converts a unix timestamp to a Python datetime object"
+ if not response:
+ return None
+ try:
+ response = int(response)
+ except ValueError:
+ return None
+ return datetime.datetime.fromtimestamp(response)
+
+def string_keys_to_dict(key_string, callback):
+ return dict([(key, callback) for key in key_string.split()])
+
+def dict_merge(*dicts):
+ merged = {}
+ [merged.update(d) for d in dicts]
+ return merged
+
+def parse_info(response):
+ "Parse the result of Redis's INFO command into a Python dict"
+ info = {}
+ def get_value(value):
+ if ',' not in value:
+ return value
+ sub_dict = {}
+ for item in value.split(','):
+ k, v = item.split('=')
+ try:
+ sub_dict[k] = int(v)
+ except ValueError:
+ sub_dict[k] = v
+ return sub_dict
+ for line in response.splitlines():
+ key, value = line.split(':')
+ try:
+ info[key] = int(value)
+ except ValueError:
+ info[key] = get_value(value)
+ return info
+
+def zset_score_pairs(response, **options):
+ """
+ If ``withscores`` is specified in the options, return the response as
+ a list of (value, score) pairs
+ """
+ if not options['withscores']:
+ return response
+ return zip(response[::2], map(float, response[1::2]))
+
+
+class Redis(object):
+ """
+ Implementation of the Redis protocol.
+
+ This abstract class proviles a Python interface to all Redis commands
+ and an implementation of the Redis protocol.
+
+ Connection and Pipeline derive from this, implementing how
+ the commands are sent and received to the Redis server
+ """
+
+ RESPONSE_CALLBACKS = dict_merge(
+ string_keys_to_dict(
+ 'AUTH DEL EXISTS EXPIRE MOVE MSETNX RENAMENX SADD SISMEMBER SMOVE '
+ 'SETNX SREM ZADD ZREM',
+ bool
+ ),
+ string_keys_to_dict(
+ 'DECRBY INCRBY LLEN SCARD SDIFFSTORE SINTERSTORE SUNIONSTORE '
+ 'ZCARD ZREMRANGEBYSCORE',
+ int
+ ),
+ string_keys_to_dict('ZSCORE ZINCRBY', float),
+ string_keys_to_dict(
+ 'FLUSHALL FLUSHDB LPUSH LSET LTRIM MSET RENAME RPUSH '
+ 'SAVE SELECT SET SHUTDOWN',
+ lambda r: r == 'OK'
+ ),
+ string_keys_to_dict('SDIFF SINTER SMEMBERS SUNION',
+ lambda r: r and set(r) or r
+ ),
+ string_keys_to_dict('ZRANGE ZRANGEBYSCORE ZREVRANGE', zset_score_pairs),
+ {
+ 'BGSAVE' : lambda r: r == 'Background saving started',
+ 'INFO' : parse_info,
+ 'KEYS' : lambda r: r and r.split(' ') or [],
+ 'LASTSAVE' : timestamp_to_datetime,
+ 'PING' : lambda r: r == 'PONG',
+ 'RANDOMKEY' : lambda r: r and r or None,
+ 'TTL' : lambda r: r != -1 and r or None,
+ }
+ )
+
+ def __init__(self, host='localhost', port=6379,
+ db=0, password=None,
+ charset='utf-8', errors='strict'):
+ self.encoding = charset
+ self.errors = errors
+ self.select(host, port, db, password)
+
+ def pipeline(self):
+ return Pipeline(self.connection, self.encoding, self.errors)
+
+ #### COMMAND EXECUTION AND PROTOCOL PARSING ####
+ def _execute_command(self, command_name, command, **options):
+ self.connection.send(command)
+ return self.parse_response(command_name, **options)
+
+ def execute_command(self, command_name, command, **options):
+ "Sends the command to the Redis server and returns it's response"
+ try:
+ return self._execute_command(command_name, command, **options)
+ except ConnectionError:
+ self.connection.disconnect()
+ return self._execute_command(command_name, command, **options)
+
+ def _parse_response(self, command_name):
+ conn = self.connection
+ response = conn.read().strip()
+ if not response:
+ self.connection.disconnect()
+ raise ConnectionError("Socket closed on remote end")
+
+ # server returned a null value
+ if response in ('$-1', '*-1'):
+ return None
+ byte, response = response[0], response[1:]
+
+ # server returned an error
+ if byte == '-':
+ if response.startswith('ERR '):
+ response = response[4:]
+ raise ResponseError(response)
+ # single value
+ elif byte == '+':
+ return response
+ # int value
+ elif byte == ':':
+ return int(response)
+ # bulk response
+ elif byte == '$':
+ length = int(response)
+ if length == -1:
+ return None
+ response = length and conn.read(length) or ''
+ conn.read(2) # read the \r\n delimiter
+ return response
+ # multi-bulk response
+ elif byte == '*':
+ length = int(response)
+ if length == -1:
+ return None
+ return [self._parse_response(command_name) for i in range(length)]
+
+ raise InvalidResponse("Unknown response type for: %s" % command_name)
+
+ def parse_response(self, command_name, **options):
+ "Parses a response from the Redis server"
+ response = self._parse_response(command_name)
+ if command_name in self.RESPONSE_CALLBACKS:
+ return self.RESPONSE_CALLBACKS[command_name](response, **options)
+ return response
+
+ def encode(self, value):
+ "Encode ``value`` using the instance's charset"
+ if isinstance(value, str):
+ return value
+ if isinstance(value, unicode):
+ return value.encode(self.encoding, self.errors)
+ # not a string or unicode, attempt to convert to a string
+ return str(value)
+
+ def format_inline(self, *args, **options):
+ "Formats a request with the inline protocol"
+ cmd = '%s\r\n' % ' '.join([self.encode(a) for a in args])
+ return self.execute_command(args[0], cmd, **options)
+
+ def format_bulk(self, *args, **options):
+ "Formats a request with the bulk protocol"
+ bulk_value = self.encode(args[-1])
+ cmd = '%s %s\r\n%s\r\n' % (
+ ' '.join([self.encode(a) for a in args[:-1]]),
+ len(bulk_value),
+ bulk_value,
+ )
+ return self.execute_command(args[0], cmd, **options)
+
+ def format_multi_bulk(self, *args, **options):
+ "Formats the request with the multi-bulk protocol"
+ cmd_count = len(args)
+ cmds = []
+ for i in args:
+ enc_value = self.encode(i)
+ cmds.append('$%s\r\n%s\r\n' % (len(enc_value), enc_value))
+ return self.execute_command(
+ args[0],
+ '*%s\r\n%s' % (cmd_count, ''.join(cmds)),
+ **options
+ )
+
+ #### CONNECTION HANDLING ####
+ def get_connection(self, host, port, db):
+ "Returns a connection object"
+ key = '%s:%s:%s' % (host, port, db)
+ if not hasattr(_connection_manager, key):
+ setattr(_connection_manager, key, Connection(host, port, db))
+ return getattr(_connection_manager, key)
+
+ def select(self, host, port, db, password=None):
+ """
+ Switch to a different database on the current host/port
+
+ Note this method actually replaces the underlying connection object
+ prior to issuing the SELECT command. This makes sure we protect
+ the thread-safe connections
+ """
+ self.connection = self.get_connection(host, port, db)
+ if password:
+ if not self.auth(password):
+ raise AuthenticationError("Invalid Password")
+ return self.format_inline('SELECT', db)
+
+ #### SERVER INFORMATION ####
+ def auth(self, password):
+ "Authenticate with the Redis server"
+ return self.format_inline('AUTH', password)
+
+ def bgsave(self):
+ """
+ Tell the Redis server to save its data to disk. Unlike save(),
+ this method is asynchronus and returns immediately.
+ """
+ return self.format_inline('BGSAVE')
+
+ def dbsize(self):
+ "Returns the number of keys in the current database"
+ return self.format_inline('DBSIZE')
+
+ def delete(self, *names):
+ "Delete one or more keys specified by ``names``"
+ return self.format_inline('DEL', ' '.join(names))
+ __delitem__ = delete
+
+ def flush(self, all_dbs=False):
+ warnings.warn(DeprecationWarning(
+ "'flush' has been deprecated. "
+ "Use Redis.flushdb() or Redis.flushall() instead"))
+ if all_dbs:
+ return self.flushall()
+ return self.flushdb()
+
+ def flushall(self):
+ "Delete all keys in all databases on the current host"
+ return self.format_inline('FLUSHALL')
+
+ def flushdb(self):
+ "Delete all keys in the current database"
+ return self.format_inline('FLUSHDB')
+
+ def info(self):
+ "Returns a dictionary containing information about the Redis server"
+ return self.format_inline('INFO')
+
+ def lastsave(self):
+ """
+ Return a Python datetime object representing the last time the
+ Redis database was saved to disk
+ """
+ return self.format_inline('LASTSAVE')
+
+ def ping(self):
+ "Ping the Redis server"
+ return self.format_inline('PING')
+
+ def save(self):
+ """
+ Tell the Redis server to save its data to disk,
+ blocking until the save is complete
+ """
+ return self.format_inline('SAVE')
+
+ #### BASIC KEY COMMANDS ####
+ def decr(self, name, amount=1):
+ """
+ Decrements the value of ``key`` by ``amount``. If no key exists,
+ the value will be initialized as 0 - ``amount``
+ """
+ return self.format_inline('DECRBY', name, amount)
+
+ def exists(self, name):
+ "Returns a boolean indicating whether key ``name`` exists"
+ return self.format_inline('EXISTS', name)
+
+ def expire(self, name, time):
+ "Set an expire on key ``name`` for ``time`` seconds"
+ return self.format_inline('EXPIRE', name, time)
+
+ def get(self, name):
+ """
+ Return the value at key ``name``, or None of the key doesn't exist
+ """
+ return self.format_inline('GET', name)
+ __getitem__ = get
+
+ def getset(self, name, value):
+ """
+ Set the value at key ``name`` to ``value`` if key doesn't exist
+ Return the value at key ``name`` atomically
+ """
+ return self.format_bulk('GETSET', name, value)
+
+ def incr(self, name, amount=1):
+ """
+ Increments the value of ``key`` by ``amount``. If no key exists,
+ the value will be initialied as ``amount``
+ """
+ return self.format_inline('INCRBY', name, amount)
+
+ def keys(self, pattern='*'):
+ "Returns a list of keys matching ``pattern``"
+ return self.format_inline('KEYS', pattern)
+
+ def mget(self, keys, *args):
+ """
+ Returns a list of values ordered identically to ``keys``
+
+ * Passing *args to this method has been deprecated *
+ """
+ keys = list_or_args('mget', keys, args)
+ return self.format_inline('MGET', *keys)
+
+ def mset(self, mapping):
+ "Sets each key in the ``mapping`` dict to it's corresponding value"
+ items = []
+ [items.extend(pair) for pair in mapping.iteritems()]
+ return self.format_multi_bulk('MSET', *items)
+
+ def msetnx(self, mapping):
+ """
+ Sets each key in the ``mapping`` dict to it's corresponding value if
+ none of the keys are already set
+ """
+ items = []
+ [items.extend(pair) for pair in mapping.iteritems()]
+ return self.format_multi_bulk('MSETNX', *items)
+
+ def move(self, name, db):
+ "Moves the key ``name`` to a different Redis database ``db``"
+ return self.format_inline('MOVE', name, db)
+
+ def randomkey(self):
+ "Returns the name of a random key"
+ return self.format_inline('RANDOMKEY')
+
+ def rename(self, src, dst, **kwargs):
+ """
+ Rename key ``src`` to ``dst``
+
+ * The following flags have been deprecated *
+ If ``preserve`` is True, rename the key only if the destination name
+ doesn't already exist
+ """
+ if kwargs:
+ if 'preserve' in kwargs:
+ warnings.warn(DeprecationWarning(
+ "preserve option to 'rename' is deprecated, "
+ "use Redis.renamenx instead"))
+ if kwargs['preserve']:
+ return self.renamenx(src, dst)
+ return self.format_inline('RENAME', src, dst)
+
+ def renamenx(self, src, dst):
+ "Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist"
+ return self.format_inline('RENAMENX', src, dst)
+
+
+ def set(self, name, value, **kwargs):
+ """
+ Set the value at key ``name`` to ``value``
+
+ * The following flags have been deprecated *
+ If ``preserve`` is True, set the value only if key doesn't already
+ exist
+ If ``getset`` is True, set the value only if key doesn't already exist
+ and return the resulting value of key
+ """
+ if kwargs:
+ if 'getset' in kwargs:
+ warnings.warn(DeprecationWarning(
+ "getset option to 'set' is deprecated, "
+ "use Redis.getset() instead"))
+ if kwargs['getset']:
+ return self.getset(name, value)
+ if 'preserve' in kwargs:
+ warnings.warn(DeprecationWarning(
+ "preserve option to 'set' is deprecated, "
+ "use Redis.setnx() instead"))
+ if kwargs['preserve']:
+ return self.setnx(name, value)
+ return self.format_bulk('SET', name, value)
+ __setitem__ = set
+
+ def setnx(self, name, value):
+ "Set the value of key ``name`` to ``value`` if key doesn't exist"
+ return self.format_bulk('SETNX', name, value)
+
+ def ttl(self, name):
+ "Returns the number of seconds until the key ``name`` will expire"
+ return self.format_inline('TTL', name)
+
+ def type(self, name):
+ "Returns the type of key ``name``"
+ return self.format_inline('TYPE', name)
+
+
+ #### LIST COMMANDS ####
+ def blpop(self, keys, timeout=0):
+ """
+ LPOP a value off of the first non-empty list
+ named in the ``keys`` list.
+
+ If none of the lists in ``keys`` has a value to LPOP, then block
+ for ``timeout`` seconds, or until a value gets pushed on to one
+ of the lists.
+
+ If timeout is 0, then block indefinitely.
+ """
+ keys.append(timeout)
+ return self.format_inline('BLPOP', *keys)
+
+ def brpop(self, keys, timeout=0):
+ """
+ RPOP a value off of the first non-empty list
+ named in the ``keys`` list.
+
+ If none of the lists in ``keys`` has a value to LPOP, then block
+ for ``timeout`` seconds, or until a value gets pushed on to one
+ of the lists.
+
+ If timeout is 0, then block indefinitely.
+ """
+ keys.append(timeout)
+ return self.format_inline('BRPOP', *keys)
+
+ def lindex(self, name, index):
+ """
+ Return the item from list ``name`` at position ``index``
+
+ Negative indexes are supported and will return an item at the
+ end of the list
+ """
+ return self.format_inline('LINDEX', name, index)
+
+ def llen(self, name):
+ "Return the length of the list ``name``"
+ return self.format_inline('LLEN', name)
+
+ def lpop(self, name):
+ "Remove and return the first item of the list ``name``"
+ return self.format_inline('LPOP', name)
+
+ def lpush(self, name, value):
+ "Push ``value`` onto the head of the list ``name``"
+ return self.format_bulk('LPUSH', name, value)
+
+ def lrange(self, name, start, end):
+ """
+ Return a slice of the list ``name`` between
+ position ``start`` and ``end``
+
+ ``start`` and ``end`` can be negative numbers just like
+ Python slicing notation
+ """
+ return self.format_inline('LRANGE', name, start, end)
+
+ def lrem(self, name, value, num=0):
+ """
+ Remove the first ``num`` occurrences of ``value`` from list ``name``
+
+ If ``num`` is 0, then all occurrences will be removed
+ """
+ return self.format_bulk('LREM', name, num, value)
+
+ def lset(self, name, index, value):
+ "Set ``position`` of list ``name`` to ``value``"
+ return self.format_bulk('LSET', name, index, value)
+
+ def ltrim(self, name, start, end):
+ """
+ Trim the list ``name``, removing all values not within the slice
+ between ``start`` and ``end``
+
+ ``start`` and ``end`` can be negative numbers just like
+ Python slicing notation
+ """
+ return self.format_inline('LTRIM', name, start, end)
+
+ def pop(self, name, tail=False):
+ """
+ Pop and return the first or last element of list ``name``
+
+ * This method has been deprecated,
+ use Redis.lpop or Redis.rpop instead *
+ """
+ if tail:
+ return self.rpop(name)
+ return self.lpop(name)
+
+ def push(self, name, value, head=False):
+ """
+ Push ``value`` onto list ``name``.
+
+ * This method has been deprecated,
+ use Redis.lpush or Redis.rpush instead *
+ """
+ warnings.warn(DeprecationWarning(
+ "Redis.push has been deprecated, "
+ "use Redis.lpush or Redis.rpush instead"))
+ if head:
+ return self.lpush(name, value)
+ return self.rpush(name, value)
+
+ def rpop(self, name):
+ "Remove and return the last item of the list ``name``"
+ return self.format_inline('RPOP', name)
+
+ def rpoplpush(self, src, dst):
+ """
+ RPOP a value off of the ``src`` list and atomically LPUSH it
+ on to the ``dst`` list. Returns the value.
+ """
+ return self.format_inline('RPOPLPUSH', src, dst)
+
+ def rpush(self, name, value):
+ "Push ``value`` onto the tail of the list ``name``"
+ return self.format_bulk('RPUSH', name, value)
+
+ def sort(self, name, start=None, num=None, by=None, get=None,
+ desc=False, alpha=False, store=None):
+ """
+ Sort and return the list, set or sorted set at ``name``.
+
+ ``start`` and ``num`` allow for paging through the sorted data
+
+ ``by`` allows using an external key to weight and sort the items.
+ Use an "*" to indicate where in the key the item value is located
+
+ ``get`` allows for returning items from external keys rather than the
+ sorted data itself. Use an "*" to indicate where int he key
+ the item value is located
+
+ ``desc`` allows for reversing the sort
+
+ ``alpha`` allows for sorting lexographically rather than numerically
+
+ ``store`` allows for storing the result of the sort into
+ the key ``store``
+ """
+ if (start is not None and num is None) or \
+ (num is not None and start is None):
+ raise RedisError("``start`` and ``num`` must both be specified")
+
+ pieces = [name]
+ if by is not None:
+ pieces.append('BY %s' % by)
+ if start is not None and num is not None:
+ pieces.append('LIMIT %s %s' % (start, num))
+ if get is not None:
+ pieces.append('GET %s' % get)
+ if desc:
+ pieces.append('DESC')
+ if alpha:
+ pieces.append('ALPHA')
+ if store is not None:
+ pieces.append('STORE %s' % store)
+ return self.format_inline('SORT', *pieces)
+
+
+ #### SET COMMANDS ####
+ def sadd(self, name, value):
+ "Add ``value`` to set ``name``"
+ return self.format_bulk('SADD', name, value)
+
+ def scard(self, name):
+ "Return the number of elements in set ``name``"
+ return self.format_inline('SCARD', name)
+
+ def sdiff(self, keys, *args):
+ "Return the difference of sets specified by ``keys``"
+ keys = list_or_args('sdiff', keys, args)
+ return self.format_inline('SDIFF', *keys)
+
+ def sdiffstore(self, dest, keys, *args):
+ """
+ Store the difference of sets specified by ``keys`` into a new
+ set named ``dest``. Returns the number of keys in the new set.
+ """
+ keys = list_or_args('sdiffstore', keys, args)
+ return self.format_inline('SDIFFSTORE', dest, *keys)
+
+ def sinter(self, keys, *args):
+ "Return the intersection of sets specified by ``keys``"
+ keys = list_or_args('sinter', keys, args)
+ return self.format_inline('SINTER', *keys)
+
+ def sinterstore(self, dest, keys, *args):
+ """
+ Store the intersection of sets specified by ``keys`` into a new
+ set named ``dest``. Returns the number of keys in the new set.
+ """
+ keys = list_or_args('sinterstore', keys, args)
+ return self.format_inline('SINTERSTORE', dest, *keys)
+
+ def sismember(self, name, value):
+ "Return a boolean indicating if ``value`` is a member of set ``name``"
+ return self.format_bulk('SISMEMBER', name, value)
+
+ def smembers(self, name):
+ "Return all members of the set ``name``"
+ return self.format_inline('SMEMBERS', name)
+
+ def smove(self, src, dst, value):
+ "Move ``value`` from set ``src`` to set ``dst`` atomically"
+ return self.format_bulk('SMOVE', src, dst, value)
+
+ def spop(self, name):
+ "Remove and return a random member of set ``name``"
+ return self.format_inline('SPOP', name)
+
+ def srandmember(self, name):
+ "Return a random member of set ``name``"
+ return self.format_inline('SRANDMEMBER', name)
+
+ def srem(self, name, value):
+ "Remove ``value`` from set ``name``"
+ return self.format_bulk('SREM', name, value)
+
+ def sunion(self, keys, *args):
+ "Return the union of sets specifiued by ``keys``"
+ keys = list_or_args('sunion', keys, args)
+ return self.format_inline('SUNION', *keys)
+
+ def sunionstore(self, dest, keys, *args):
+ """
+ Store the union of sets specified by ``keys`` into a new
+ set named ``dest``. Returns the number of keys in the new set.
+ """
+ keys = list_or_args('sunionstore', keys, args)
+ return self.format_inline('SUNIONSTORE', dest, *keys)
+
+
+ #### SORTED SET COMMANDS ####
+ def zadd(self, name, value, score):
+ "Add member ``value`` with score ``score`` to sorted set ``name``"
+ return self.format_bulk('ZADD', name, score, value)
+
+ def zcard(self, name):
+ "Return the number of elements in the sorted set ``name``"
+ return self.format_inline('ZCARD', name)
+
+ def zincr(self, key, member, value=1):
+ "This has been deprecated, use zincrby instead"
+ warnings.warn(DeprecationWarning(
+ "Redis.zincr has been deprecated, use Redis.zincrby instead"
+ ))
+ return self.zincrby(key, member, value)
+
+ def zincrby(self, name, value, amount=1):
+ "Increment the score of ``value`` in sorted set ``name`` by ``amount``"
+ return self.format_bulk('ZINCRBY', name, amount, value)
+
+ def zrange(self, name, start, end, desc=False, withscores=False):
+ """
+ Return a range of values from sorted set ``name`` between
+ ``start`` and ``end`` sorted in ascending order.
+
+ ``start`` and ``end`` can be negative, indicating the end of the range.
+
+ ``desc`` indicates to sort in descending order.
+
+ ``withscores`` indicates to return the scores along with the values.
+ The return type is a list of (value, score) pairs
+ """
+ if desc:
+ return self.zrevrange(name, start, end, withscores)
+ pieces = ['ZRANGE', name, start, end]
+ if withscores:
+ pieces.append('withscores')
+ return self.format_inline(*pieces, **{'withscores' : withscores})
+
+ def zrangebyscore(self, name, min, max, start=None, num=None, withscores=False):
+ """
+ Return a range of values from the sorted set ``name`` with scores
+ between ``min`` and ``max``.
+
+ If ``start`` and ``num`` are specified, then return a slice of the range.
+
+ ``withscores`` indicates to return the scores along with the values.
+ The return type is a list of (value, score) pairs
+ """
+ if (start is not None and num is None) or \
+ (num is not None and start is None):
+ raise RedisError("``start`` and ``num`` must both be specified")
+ pieces = ['ZRANGEBYSCORE', name, min, max]
+ if start is not None and num is not None:
+ pieces.extend(['LIMIT', start, num])
+ if withscores:
+ pieces.append('withscores')
+ return self.format_inline(*pieces, **{'withscores' : withscores})
+
+ def zrem(self, name, value):
+ "Remove member ``value`` from sorted set ``name``"
+ return self.format_bulk('ZREM', name, value)
+
+ def zremrangebyscore(self, name, min, max):
+ """
+ Remove all elements in the sorted set ``name`` with scores
+ between ``min`` and ``max``
+ """
+ return self.format_inline('ZREMRANGEBYSCORE', name, min, max)
+
+ def zrevrange(self, name, start, num, withscores=False):
+ """
+ Return a range of values from sorted set ``name`` between
+ ``start`` and ``num`` sorted in descending order.
+
+ ``start`` and ``num`` can be negative, indicating the end of the range.
+
+ ``withscores`` indicates to return the scores along with the values
+ as a dictionary of value => score
+ """
+ pieces = ['ZREVRANGE', name, start, num]
+ if withscores:
+ pieces.append('withscores')
+ return self.format_inline(*pieces, **{'withscores' : withscores})
+
+ def zscore(self, name, value):
+ "Return the score of element ``value`` in sorted set ``name``"
+ return self.format_bulk('ZSCORE', name, value)
+
+
+class Pipeline(Redis):
+ """
+ Pipelines provide a way to transmit multiple commands to the Redis server
+ in one transmission. This is convenient for batch processing, such as
+ saving all the values in a list to Redis.
+
+ Note that pipelining does *not* guarentee all the commands will be executed
+ together atomically, nor does it guarentee any transactional consistency.
+ If the third command in the batch fails, the first two will still have been
+ executed and "committed"
+ """
+ def __init__(self, connection, charset, errors):
+ self.connection = connection
+ self.command_stack = []
+ self.encoding = charset
+ self.errors = errors
+
+ def execute_command(self, command_name, command, **options):
+ """
+ Stage a command to be executed when execute() is next called
+
+ Returns the current Pipeline object back so commands can be
+ chained together, such as:
+
+ pipe = pipe.set('foo', 'bar').incr('baz').decr('bang')
+
+ At some other point, you can then run: pipe.execute(),
+ which will execute all commands queued in the pipe.
+ """
+ self.command_stack.append((command_name, command, options))
+ return self
+
+ def _execute(self, commands):
+ for _, command, options in commands:
+ self.connection.send(command)
+ return [self.parse_response(name, **options)
+ for name, _, options in commands]
+
+ def execute(self):
+ "Execute all the commands in the current pipeline"
+ stack = self.command_stack
+ self.command_stack = []
+ try:
+ return self._execute(stack)
+ except ConnectionError:
+ self.connection.disconnect()
+ return self._execute(stack)
+
+ def select(self, host, port, db):
+ raise RedisError("Cannot select a different database from a pipeline")
+ \ No newline at end of file
diff --git a/redis/exceptions.py b/redis/exceptions.py
index acbe7b7..d3449b6 100644
--- a/redis/exceptions.py
+++ b/redis/exceptions.py
@@ -1,5 +1,20 @@
-class RedisError(Exception): pass
-class ConnectionError(RedisError): pass
-class ResponseError(RedisError): pass
-class InvalidResponse(RedisError): pass
-class InvalidData(RedisError): pass
+"Core exceptions raised by the Redis client"
+
+class RedisError(Exception):
+ pass
+
+class AuthenticationError(RedisError):
+ pass
+
+class ConnectionError(RedisError):
+ pass
+
+class ResponseError(RedisError):
+ pass
+
+class InvalidResponse(RedisError):
+ pass
+
+class InvalidData(RedisError):
+ pass
+ \ No newline at end of file
diff --git a/setup.py b/setup.py
index 4ecc27a..7ecf4ae 100644
--- a/setup.py
+++ b/setup.py
@@ -2,8 +2,8 @@
"""
@file setup.py
-@author Paul Hubbard
-@date 10/2/09
+@author Andy McCurdy
+@date 2/12/2010
@brief Setuptools configuration for redis client
"""
@@ -13,13 +13,17 @@ sdict = {
'name' : 'redis',
'version' : version,
'description' : 'Python client for Redis key-value store',
+ 'long_description' : 'Python client for Redis key-value store',
'url': 'http://github.com/andymccurdy/redis-py',
'download_url' : 'http://cloud.github.com/downloads/andymccurdy/redis-py/redis-%s.tar.gz' % version,
'author' : 'Andy McCurdy',
'author_email' : 'sedrik@gmail.com',
'maintainer' : 'Andy McCurdy',
'maintainer_email' : 'sedrik@gmail.com',
- 'keywords': ['Redis', 'key-value store'],
+ 'keywords' : ['Redis', 'key-value store'],
+ 'license' : 'MIT',
+ 'py_modules' : ['redis'],
+ 'test_suite' : 'tests.all_tests',
'classifiers' : [
'Development Status :: 4 - Beta',
'Environment :: Console',
@@ -33,17 +37,6 @@ try:
from setuptools import setup
except ImportError:
from distutils.core import setup
-#setup(**setupdict)
-setup(name=sdict['name'],
- version=sdict['version'],
- author=sdict['author'],
- author_email=sdict['author_email'],
- maintainer=sdict['maintainer'],
- maintainer_email=sdict['maintainer_email'],
- url=sdict['url'],
- classifiers=sdict['classifiers'],
- description=sdict['description'],
- long_description=sdict['description'],
- download_url=sdict['download_url'],
- license='MIT',
- py_modules = ['redis'])
+
+setup(**sdict)
+
diff --git a/tests.py b/tests.py
deleted file mode 100644
index 2fd8aee..0000000
--- a/tests.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import redis
-
-# legacy doctests
-redis.old_client.run_doctests(redis.old_client)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..7c2e74f
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,7 @@
+import unittest
+from server_commands import ServerCommands
+
+def all_tests():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(ServerCommands))
+ return suite
diff --git a/tests/server_commands.py b/tests/server_commands.py
new file mode 100644
index 0000000..5a0d6f0
--- /dev/null
+++ b/tests/server_commands.py
@@ -0,0 +1,637 @@
+import redis
+import unittest
+import datetime
+
+class ServerCommands(unittest.TestCase):
+
+ def setUp(self):
+ self.client = redis.Redis(host='localhost', port=6379, db=9)
+ self.client.flushdb()
+
+ def tearDown(self):
+ self.client.flushdb()
+
+ # GENERAL SERVER COMMANDS
+ def test_dbsize(self):
+ self.client['a'] = 'foo'
+ self.client['b'] = 'bar'
+ self.assertEquals(self.client.dbsize(), 2)
+
+ def test_get_and_set(self):
+ # get and set can't be tested independently of each other
+ self.assertEquals(self.client.get('a'), None)
+ byte_string = 'value'
+ integer = 5
+ unicode_string = unichr(3456) + u'abcd' + unichr(3421)
+ self.assert_(self.client.set('byte_string', byte_string))
+ self.assert_(self.client.set('integer', 5))
+ self.assert_(self.client.set('unicode_string', unicode_string))
+ self.assertEquals(self.client.get('byte_string'), byte_string)
+ self.assertEquals(self.client.get('integer'), str(integer))
+ self.assertEquals(self.client.get('unicode_string').decode('utf-8'), unicode_string)
+
+ def test_getitem_and_setitem(self):
+ self.client['a'] = 'bar'
+ self.assertEquals(self.client['a'], 'bar')
+
+ def test_delete(self):
+ self.assertEquals(self.client.delete('a'), False)
+ self.client['a'] = 'foo'
+ self.assertEquals(self.client.delete('a'), True)
+
+ def test_delitem(self):
+ self.client['a'] = 'foo'
+ del self.client['a']
+ self.assertEquals(self.client['a'], None)
+
+ def test_info(self):
+ self.client['a'] = 'foo'
+ self.client['b'] = 'bar'
+ info = self.client.info()
+ self.assert_(isinstance(info, dict))
+ self.assertEquals(info['db9']['keys'], 2)
+
+ def test_lastsave(self):
+ self.assert_(isinstance(self.client.lastsave(), datetime.datetime))
+
+ def test_ping(self):
+ self.assertEquals(self.client.ping(), True)
+
+
+ # KEYS
+ def test_decr(self):
+ self.assertEquals(self.client.decr('a'), -1)
+ self.assertEquals(self.client['a'], '-1')
+ self.assertEquals(self.client.decr('a'), -2)
+ self.assertEquals(self.client['a'], '-2')
+ self.assertEquals(self.client.decr('a', amount=5), -7)
+ self.assertEquals(self.client['a'], '-7')
+
+ def test_exists(self):
+ self.assertEquals(self.client.exists('a'), False)
+ self.client['a'] = 'foo'
+ self.assertEquals(self.client.exists('a'), True)
+
+ def expire(self):
+ self.assertEquals(self.client.expire('a'), False)
+ self.client['a'] = 'foo'
+ self.assertEquals(self.client.expire('a'), True)
+
+ def test_getset(self):
+ self.assertEquals(self.client.getset('a', 'foo'), None)
+ self.assertEquals(self.client.getset('a', 'bar'), 'foo')
+
+ def test_incr(self):
+ self.assertEquals(self.client.incr('a'), 1)
+ self.assertEquals(self.client['a'], '1')
+ self.assertEquals(self.client.incr('a'), 2)
+ self.assertEquals(self.client['a'], '2')
+ self.assertEquals(self.client.incr('a', amount=5), 7)
+ self.assertEquals(self.client['a'], '7')
+
+ def test_keys(self):
+ self.assertEquals(self.client.keys(), [])
+ keys = set(['test_a', 'test_b', 'testc'])
+ for key in keys:
+ self.client[key] = 1
+ self.assertEquals(set(self.client.keys(pattern='test_*')),
+ keys - set(['testc']))
+ self.assertEquals(set(self.client.keys(pattern='test*')), keys)
+
+ def test_mget(self):
+ self.assertEquals(self.client.mget(['a', 'b']), [None, None])
+ self.client['a'] = '1'
+ self.client['b'] = '2'
+ self.client['c'] = '3'
+ self.assertEquals(self.client.mget(['a', 'other', 'b', 'c']),
+ ['1', None, '2', '3'])
+
+ def test_mset(self):
+ d = {'a' : '1', 'b' : '2', 'c' : '3'}
+ self.assert_(self.client.mset(d))
+ for k,v in d.iteritems():
+ self.assertEquals(self.client[k], v)
+
+ def test_msetnx(self):
+ d = {'a' : '1', 'b' : '2', 'c' : '3'}
+ self.assert_(self.client.msetnx(d))
+ d2 = {'a' : 'x', 'd' : '4'}
+ self.assert_(not self.client.msetnx(d2))
+ for k,v in d.iteritems():
+ self.assertEquals(self.client[k], v)
+ self.assertEquals(self.client['d'], None)
+
+ def test_randomkey(self):
+ self.assertEquals(self.client.randomkey(), None)
+ self.client['a'] = '1'
+ self.client['b'] = '2'
+ self.client['c'] = '3'
+ self.assert_(self.client.randomkey() in ('a', 'b', 'c'))
+
+ def test_rename(self):
+ self.client['a'] = '1'
+ self.assert_(self.client.rename('a', 'b'))
+ self.assertEquals(self.client['a'], None)
+ self.assertEquals(self.client['b'], '1')
+
+ def test_renamenx(self):
+ self.client['a'] = '1'
+ self.client['b'] = '2'
+ self.assert_(not self.client.renamenx('a', 'b'))
+ self.assertEquals(self.client['a'], '1')
+ self.assertEquals(self.client['b'], '2')
+
+ def test_setnx(self):
+ self.assert_(self.client.setnx('a', '1'))
+ self.assertEquals(self.client['a'], '1')
+ self.assert_(not self.client.setnx('a', '2'))
+ self.assertEquals(self.client['a'], '1')
+
+ def test_ttl(self):
+ self.assertEquals(self.client.ttl('a'), None)
+ self.client['a'] = '1'
+ self.assertEquals(self.client.ttl('a'), None)
+ self.client.expire('a', 10)
+ # this could potentially fail if for some reason there's a gap of
+ # time between these commands.
+ self.assertEquals(self.client.ttl('a'), 10)
+
+ def test_type(self):
+ self.assertEquals(self.client.type('a'), 'none')
+ self.client['a'] = '1'
+ self.assertEquals(self.client.type('a'), 'string')
+ del self.client['a']
+ self.client.lpush('a', '1')
+ self.assertEquals(self.client.type('a'), 'list')
+ del self.client['a']
+ self.client.sadd('a', '1')
+ self.assertEquals(self.client.type('a'), 'set')
+ del self.client['a']
+ self.client.zadd('a', '1', 1)
+ self.assertEquals(self.client.type('a'), 'zset')
+
+ # LISTS
+ def make_list(self, name, l):
+ for i in l:
+ self.client.rpush(name, i)
+
+ def test_blpop(self):
+ self.make_list('a', 'ab')
+ self.make_list('b', 'cd')
+ self.assertEquals(self.client.blpop(['b', 'a'], timeout=1), ['b', 'c'])
+ self.assertEquals(self.client.blpop(['b', 'a'], timeout=1), ['b', 'd'])
+ self.assertEquals(self.client.blpop(['b', 'a'], timeout=1), ['a', 'a'])
+ self.assertEquals(self.client.blpop(['b', 'a'], timeout=1), ['a', 'b'])
+ self.assertEquals(self.client.blpop(['b', 'a'], timeout=1), None)
+
+ def test_brpop(self):
+ self.make_list('a', 'ab')
+ self.make_list('b', 'cd')
+ self.assertEquals(self.client.brpop(['b', 'a'], timeout=1), ['b', 'd'])
+ self.assertEquals(self.client.brpop(['b', 'a'], timeout=1), ['b', 'c'])
+ self.assertEquals(self.client.brpop(['b', 'a'], timeout=1), ['a', 'b'])
+ self.assertEquals(self.client.brpop(['b', 'a'], timeout=1), ['a', 'a'])
+ self.assertEquals(self.client.brpop(['b', 'a'], timeout=1), None)
+
+ def test_lindex(self):
+ # no key
+ self.assertEquals(self.client.lindex('a', '0'), None)
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.lindex, 'a', '0')
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abc')
+ self.assertEquals(self.client.lindex('a', '0'), 'a')
+ self.assertEquals(self.client.lindex('a', '1'), 'b')
+ self.assertEquals(self.client.lindex('a', '2'), 'c')
+
+ def test_llen(self):
+ # no key
+ self.assertEquals(self.client.llen('a'), 0)
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.llen, 'a')
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abc')
+ self.assertEquals(self.client.llen('a'), 3)
+
+ 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_lpush(self):
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.lpush, 'a', 'a')
+ del self.client['a']
+ # real logic
+ self.assert_(self.client.lpush('a', 'b'))
+ self.assert_(self.client.lpush('a', 'a'))
+ self.assertEquals(self.client.lindex('a', 0), 'a')
+ self.assertEquals(self.client.lindex('a', 1), 'b')
+
+ def test_lrange(self):
+ # no key
+ self.assertEquals(self.client.lrange('a', 0, 1), None)
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.lrange, 'a', 0, 1)
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abcde')
+ self.assertEquals(self.client.lrange('a', 0, 2), ['a', 'b', 'c'])
+ self.assertEquals(self.client.lrange('a', 2, 10), ['c', 'd', 'e'])
+
+ def test_lrem(self):
+ # no key
+ self.assertEquals(self.client.lrem('a', 'foo'), 0)
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.lrem, 'a', 'b')
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'aaaa')
+ self.assertEquals(self.client.lrem('a', 'a', 1), 1)
+ self.assertEquals(self.client.lrange('a', 0, 3), ['a', 'a', 'a'])
+ self.assertEquals(self.client.lrem('a', 'a'), 3)
+ self.assertEquals(self.client.lrange('a', 0, 1), [])
+
+ def test_lset(self):
+ # no key
+ self.assertRaises(redis.ResponseError, self.client.lset, 'a', 1, 'b')
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.lset, 'a', 1, 'b')
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abc')
+ self.assertEquals(self.client.lrange('a', 0, 2), ['a', 'b', 'c'])
+ self.assert_(self.client.lset('a', 1, 'd'))
+ self.assertEquals(self.client.lrange('a', 0, 2), ['a', 'd', 'c'])
+
+ def test_ltrim(self):
+ # no key -- TODO: Not sure why this is actually true.
+ self.assert_(self.client.ltrim('a', 0, 2))
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.ltrim, 'a', 0, 2)
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abc')
+ self.assert_(self.client.ltrim('a', 0, 1))
+ self.assertEquals(self.client.lrange('a', 0, 5), ['a', 'b'])
+
+ def test_pop(self):
+ # no key
+ self.assertEquals(self.client.pop('a'), None)
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.pop, 'a')
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abc')
+ self.assertEquals(self.client.pop('a'), 'a')
+ self.assertEquals(self.client.pop('a'), 'b')
+ self.assertEquals(self.client.pop('a'), 'c')
+ self.assertEquals(self.client.pop('a'), None)
+
+ def test_rpop(self):
+ # no key
+ self.assertEquals(self.client.rpop('a'), None)
+ # key is not a list
+ self.client['a'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.rpop, 'a')
+ del self.client['a']
+ # real logic
+ self.make_list('a', 'abc')
+ self.assertEquals(self.client.rpop('a'), 'c')
+ self.assertEquals(self.client.rpop('a'), 'b')
+ self.assertEquals(self.client.rpop('a'), 'a')
+ self.assertEquals(self.client.rpop('a'), None)
+
+ def test_rpoplpush(self):
+ # no src key
+ self.make_list('b', ['b1'])
+ self.assertEquals(self.client.rpoplpush('a', 'b'), None)
+ # no dest key
+ self.assertEquals(self.client.rpoplpush('b', 'a'), 'b1')
+ self.assertEquals(self.client.lindex('a', 0), 'b1')
+ del self.client['a']
+ del self.client['b']
+ # src key is not a list
+ self.client['a'] = 'a1'
+ self.assertRaises(redis.ResponseError, self.client.rpoplpush, 'a', 'b')
+ del self.client['a']
+ # dest key is not a list
+ self.make_list('a', ['a1'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.rpoplpush, 'a', 'b')
+ del self.client['a']
+ del self.client['b']
+ # real logic
+ self.make_list('a', ['a1', 'a2', 'a3'])
+ self.make_list('b', ['b1', 'b2', 'b3'])
+ self.assertEquals(self.client.rpoplpush('a', 'b'), 'a3')
+ self.assertEquals(self.client.lrange('a', 0, 2), ['a1', 'a2'])
+ self.assertEquals(self.client.lrange('b', 0, 4),
+ ['a3', 'b1', 'b2', 'b3'])
+
+ # Set commands
+ def make_set(self, name, l):
+ for i in l:
+ self.client.sadd(name, i)
+
+ def test_sadd(self):
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.sadd, 'a', 'a1')
+ del self.client['a']
+ # real logic
+ members = set(['a1', 'a2', 'a3'])
+ self.make_set('a', members)
+ self.assertEquals(self.client.smembers('a'), members)
+
+ def test_scard(self):
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.scard, 'a')
+ del self.client['a']
+ # real logic
+ self.make_set('a', 'abc')
+ self.assertEquals(self.client.scard('a'), 3)
+
+ def test_sdiff(self):
+ # some key is not a set
+ self.make_set('a', ['a1', 'a2', 'a3'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.sdiff, ['a', 'b'])
+ del self.client['b']
+ # real logic
+ self.make_set('b', ['b1', 'a2', 'b3'])
+ self.assertEquals(self.client.sdiff(['a', 'b']), set(['a1', 'a3']))
+
+ def test_sdiffstore(self):
+ # some key is not a set
+ self.make_set('a', ['a1', 'a2', 'a3'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.sdiffstore,
+ 'c', ['a', 'b'])
+ del self.client['b']
+ self.make_set('b', ['b1', 'a2', 'b3'])
+ # dest key always gets overwritten, even if it's not a set, so don't
+ # test for that
+ # real logic
+ self.assertEquals(self.client.sdiffstore('c', ['a', 'b']), 2)
+ self.assertEquals(self.client.smembers('c'), set(['a1', 'a3']))
+
+ def test_sinter(self):
+ # some key is not a set
+ self.make_set('a', ['a1', 'a2', 'a3'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.sinter, ['a', 'b'])
+ del self.client['b']
+ # real logic
+ self.make_set('b', ['a1', 'b2', 'a3'])
+ self.assertEquals(self.client.sinter(['a', 'b']), set(['a1', 'a3']))
+
+ def test_sinterstore(self):
+ # some key is not a set
+ self.make_set('a', ['a1', 'a2', 'a3'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.sinterstore,
+ 'c', ['a', 'b'])
+ del self.client['b']
+ self.make_set('b', ['a1', 'b2', 'a3'])
+ # dest key always gets overwritten, even if it's not a set, so don't
+ # test for that
+ # real logic
+ self.assertEquals(self.client.sinterstore('c', ['a', 'b']), 2)
+ self.assertEquals(self.client.smembers('c'), set(['a1', 'a3']))
+
+ def test_sismember(self):
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.sismember, 'a', 'a')
+ del self.client['a']
+ # real logic
+ self.make_set('a', 'abc')
+ self.assertEquals(self.client.sismember('a', 'a'), True)
+ self.assertEquals(self.client.sismember('a', 'b'), True)
+ self.assertEquals(self.client.sismember('a', 'c'), True)
+ self.assertEquals(self.client.sismember('a', 'd'), False)
+
+ def test_smembers(self):
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.smembers, 'a')
+ del self.client['a']
+ # real logic
+ self.make_set('a', 'abc')
+ self.assertEquals(self.client.smembers('a'), set(['a', 'b', 'c']))
+
+ def test_smove(self):
+ # src key is not set
+ self.make_set('b', ['b1', 'b2'])
+ self.assertEquals(self.client.smove('a', 'b', 'a1'), 0)
+ # src key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.smove,
+ 'a', 'b', 'a1')
+ del self.client['a']
+ self.make_set('a', ['a1', 'a2'])
+ # dest key is not a set
+ del self.client['b']
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.smove,
+ 'a', 'b', 'a1')
+ del self.client['b']
+ self.make_set('b', ['b1', 'b2'])
+ # real logic
+ self.assert_(self.client.smove('a', 'b', 'a1'))
+ self.assertEquals(self.client.smembers('a'), set(['a2']))
+ self.assertEquals(self.client.smembers('b'), set(['b1', 'b2', 'a1']))
+
+ def test_spop(self):
+ # key is not set
+ self.assertEquals(self.client.spop('a'), None)
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.spop, 'a')
+ del self.client['a']
+ # real logic
+ self.make_set('a', 'abc')
+ value = self.client.spop('a')
+ self.assert_(value in 'abc')
+ self.assertEquals(self.client.smembers('a'), set('abc') - set(value))
+
+ def test_srandmember(self):
+ # key is not set
+ self.assertEquals(self.client.srandmember('a'), None)
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.srandmember, 'a')
+ del self.client['a']
+ # real logic
+ self.make_set('a', 'abc')
+ self.assert_(self.client.srandmember('a') in 'abc')
+
+ def test_srem(self):
+ # key is not set
+ self.assertEquals(self.client.srem('a', 'a'), False)
+ # key is not a set
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.srem, 'a', 'a')
+ del self.client['a']
+ # real logic
+ self.make_set('a', 'abc')
+ self.assertEquals(self.client.srem('a', 'd'), False)
+ self.assertEquals(self.client.srem('a', 'b'), True)
+ self.assertEquals(self.client.smembers('a'), set('ac'))
+
+ def test_sunion(self):
+ # some key is not a set
+ self.make_set('a', ['a1', 'a2', 'a3'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.sunion, ['a', 'b'])
+ del self.client['b']
+ # real logic
+ self.make_set('b', ['a1', 'b2', 'a3'])
+ self.assertEquals(self.client.sunion(['a', 'b']),
+ set(['a1', 'a2', 'a3', 'b2']))
+
+ def test_sunionstore(self):
+ # some key is not a set
+ self.make_set('a', ['a1', 'a2', 'a3'])
+ self.client['b'] = 'b'
+ self.assertRaises(redis.ResponseError, self.client.sunionstore,
+ 'c', ['a', 'b'])
+ del self.client['b']
+ self.make_set('b', ['a1', 'b2', 'a3'])
+ # dest key always gets overwritten, even if it's not a set, so don't
+ # test for that
+ # real logic
+ self.assertEquals(self.client.sunionstore('c', ['a', 'b']), 4)
+ self.assertEquals(self.client.smembers('c'),
+ set(['a1', 'a2', 'a3', 'b2']))
+
+ # SORTED SETS
+ def make_zset(self, name, d):
+ for k,v in d.items():
+ self.client.zadd(name, k, v)
+
+ def test_zadd(self):
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zrange('a', 0, 3), ['a1', 'a2', 'a3'])
+
+ def test_zcard(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zcard, 'a')
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zcard('a'), 3)
+
+ def test_zincrby(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zincrby, 'a', 'a1')
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zincrby('a', 'a2'), 3.0)
+ self.assertEquals(self.client.zincrby('a', 'a3', amount=5), 8.0)
+ self.assertEquals(self.client.zscore('a', 'a2'), 3.0)
+ self.assertEquals(self.client.zscore('a', 'a3'), 8.0)
+
+ def test_zrange(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zrange, 'a', 0, 1)
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zrange('a', 0, 1), ['a1', 'a2'])
+ self.assertEquals(self.client.zrange('a', 1, 2), ['a2', 'a3'])
+ self.assertEquals(self.client.zrange('a', 0, 1, withscores=True),
+ [('a1', 1.0), ('a2', 2.0)])
+ self.assertEquals(self.client.zrange('a', 1, 2, withscores=True),
+ [('a2', 2.0), ('a3', 3.0)])
+
+ def test_zrangebyscore(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zrangebyscore,
+ 'a', 0, 1)
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3, 'a4' : 4, 'a5' : 5})
+ self.assertEquals(self.client.zrangebyscore('a', 2, 4),
+ ['a2', 'a3', 'a4'])
+ self.assertEquals(self.client.zrangebyscore('a', 2, 4, start=1, num=2),
+ ['a3', 'a4'])
+ self.assertEquals(self.client.zrangebyscore('a', 2, 4, withscores=True),
+ [('a2', 2.0), ('a3', 3.0), ('a4', 4.0)])
+
+ def test_zrem(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zrem, 'a', 'a1')
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zrem('a', 'a2'), True)
+ self.assertEquals(self.client.zrange('a', 0, 5), ['a1', 'a3'])
+ self.assertEquals(self.client.zrem('a', 'b'), False)
+ self.assertEquals(self.client.zrange('a', 0, 5), ['a1', 'a3'])
+
+ def test_zremrangebyscore(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zremrangebyscore,
+ 'a', 0, 1)
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3, 'a4' : 4, 'a5' : 5})
+ self.assertEquals(self.client.zremrangebyscore('a', 2, 4), 3)
+ self.assertEquals(self.client.zrange('a', 0, 5), ['a1', 'a5'])
+ self.assertEquals(self.client.zremrangebyscore('a', 2, 4), 0)
+ self.assertEquals(self.client.zrange('a', 0, 5), ['a1', 'a5'])
+
+ def test_zrevrange(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zrevrange,
+ 'a', 0, 1)
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zrevrange('a', 0, 1), ['a3', 'a2'])
+ self.assertEquals(self.client.zrevrange('a', 1, 2), ['a2', 'a1'])
+ self.assertEquals(self.client.zrevrange('a', 0, 1, withscores=True),
+ [('a3', 3.0), ('a2', 2.0)])
+ self.assertEquals(self.client.zrevrange('a', 1, 2, withscores=True),
+ [('a2', 2.0), ('a1', 1.0)])
+
+ def test_zscore(self):
+ # key is not a zset
+ self.client['a'] = 'a'
+ self.assertRaises(redis.ResponseError, self.client.zscore, 'a', 'a1')
+ del self.client['a']
+ # real logic
+ self.make_zset('a', {'a1' : 1, 'a2' : 2, 'a3' : 3})
+ self.assertEquals(self.client.zscore('a', 'a2'), 2.0)
+
+