Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'master' of github.com:ask/redish

Conflicts:
	redish/types.py
  • Loading branch information...
commit 04b22ef970a080bc23c2d9cfba781cff51aa5127 2 parents 0d30a85 + 6136bab
Ask Solem authored
Showing with 149 additions and 11 deletions.
  1. +61 −0 README.rst
  2. +58 −2 redish/proxy.py
  3. +30 −9 redish/types.py
View
61 README.rst
@@ -495,6 +495,67 @@ score)`` tuples::
>>> z.range_by_score(min=0.3, max=0.6)
['bar', 'xuzzy']
+redish.proxy
+============
+
+The proxy submodule offers a different view on the redis datastore: it exposes
+the strings, integers, lists, hashes, sets and sorted sets within the
+datastore transparently, as if they were native Python objects accessed by key
+on the proxy object. They do not store serialized objects as with the rest of
+redish. For example::
+
+ >>> from redish import proxy
+ >>> r = proxy.Proxy()
+
+Key access yields an object that acts like the Python equivalent of the
+underlying Redis structure. That structure can be read and modified as if it
+is native, local object. Here, that object acts like a dict::
+
+ >>> r['mydict']
+ {'father': 'Frank Costanza', 'name': 'George Louis Costanza', 'mother': 'Estelle Costanza'}
+ >>> r['mydict']['name']
+ 'George Louis Costanza'
+ >>> r['mydict']['name'] = "Georgie"
+ >>> r['mydict']['name']
+ 'Georgie'
+
+Sometimes, it may be convenient to assign a variable to the proxy object, and
+use that in subsequent operations::
+
+ >>> ss = r['myset']
+ >>> 'George' in ss
+ True
+ >>> 'Ringo' in ss
+ False
+
+The Proxy object is a subclass of a normal redis.Client object, and so
+supports the same methods (other than `__getitem__`, `__setitem__`,
+`__contains__`, and `__delitem__`). The object that the proxy object returns
+is an instance of one of the classes from redish.types (with the exception of
+unicode: those are simply serialized/unserialized from the underlying redis
+data store as UTF-8).
+::
+
+ >>> r['mycounter'] = 1
+ >>> cc = r['mycounter']
+ >>> cc += 1
+ >>> cc += 1
+ >>> r.get('mycounter')
+ '3'
+ >>> type(cc)
+ <class 'redish.types.Int'>
+
+Since redis does not support empty sets, lists, or hashes, the proxy object
+will (thread-)locally 'remember' keys that are explicitly set as empty types.
+It does not currently remember container types that have been emptied as a
+product of operations on the underlying store::
+
+ >>> r['newlist'] = []
+ >>> r['newlist'].extend([1,2])
+ >>> len(r['newlist'])
+ 2
+
+For more information, see the redish.proxy documentation.
Installation
============
View
60 redish/proxy.py
@@ -24,6 +24,13 @@
"hash": types.Dict,
}
+REV_TYPE_MAP = {
+ list: types.List,
+ set: types.Set,
+ dict: types.Dict,
+ types.ZSet: types.SortedSet,
+}
+
def int_or_str(thing, key, client):
try:
int(thing)
@@ -31,30 +38,58 @@ def int_or_str(thing, key, client):
except (TypeError, ValueError):
return decode(thing, "UTF-8")
+class Glob(str):
+ pass
+
class Proxy(Redis):
"""Acts as the Redis object except with basic item access or assignment.
In those cases, transparently returns an object that mimics its
associated Python type or passes assignments to the backing store."""
+ def __init__(self, *args, **kwargs):
+ """
+ If a user attempts to initialize a redis key with an empty container,
+ that container is kept in the (local thread's) proxy object so that
+ subsequent accesses keep the right type without throwing KeyErrors.
+ """
+ self.empties = {}
+ super(Proxy, self).__init__(*args, **kwargs)
+
def __getitem__(self, key):
"""Return a proxy type according to the native redis type
associated with the key."""
+ if isinstance(key, Glob):
+ return self.multikey(key)
typ = self.type(key)
+ if typ == 'string':
+ # because strings can be empty, check before "empties"
+ return int_or_str(self.get(key), key, self)
+ if key in self.empties:
+ if typ == 'none':
+ return self.empties[key]
+ else:
+ self.empties.pop(key)
if typ == 'none':
raise KeyError(key)
- elif typ == 'string':
- return int_or_str(self.get(key), key, self)
else:
return TYPE_MAP[typ](key, self)
def __setitem__(self, key, value):
"""Copy the contents of the value into the redis store."""
+ if key in self.empties:
+ del self.empties[key]
if isinstance(value, (int, types.Int)):
self.set(key, int(value))
return
elif isinstance(value, basestring):
self.set(key, encode(value, "UTF-8"))
return
+ if not value:
+ if self.exists(key):
+ self.delete(key)
+ if value != None:
+ self.empties[key] = REV_TYPE_MAP[type(value)](key, self)
+ return
pline = self.pipeline()
if self.exists(key):
pline = pline.delete(key)
@@ -71,3 +106,24 @@ def __setitem__(self, key, value):
pline = pline.zadd(key, k, v)
pline.execute()
+ def __contains__(self, key):
+ """
+ We check for existence within the *proxy object*, and so we
+ must look in both the backing store and the object's "empties."
+ """
+ return self.exists(key) or key in self.empties
+
+ def __delitem__(self, k):
+ if isinstance(k, Glob):
+ keys = self.keys(k)
+ else:
+ keys = [k]
+ for key in keys:
+ if key in self.empties:
+ del self.empties[key]
+ self.delete(*keys)
+
+ def multikey(self, pattern):
+ for p in self.keys(pattern):
+ yield self[p]
+
View
39 redish/types.py
@@ -1,6 +1,7 @@
-from Queue import Empty, Full
import bisect
+from Queue import Empty, Full
+
from redis.exceptions import ResponseError
from redish.utils import mkey
@@ -61,6 +62,8 @@ def __getslice__(self, i, j):
def _as_list(self):
return self.client.lrange(self.name, 0, -1)
+ copy = _as_list
+
def append(self, value):
"""Add ``value`` to the end of the list."""
return self.client.rpush(self.name, value)
@@ -131,6 +134,8 @@ def __len__(self):
def _as_set(self):
return self.client.smembers(self.name)
+ copy = _as_set
+
def add(self, member):
"""Add element to set.
@@ -164,8 +169,13 @@ def union(self, other):
(i.e. all elements that are in either set.)
+ Operates on either redish.types.Set or __builtins__.set.
+
"""
- return self.client.sunion([self.name, other.name])
+ if isinstance(other, self.__class__):
+ return self.client.sunion([self.name, other.name])
+ else:
+ return self._as_set().union(other)
def update(self, other):
"""Update this set with the union of itself and others."""
@@ -179,8 +189,13 @@ def intersection(self, other):
(i.e. all elements that are in both sets.)
+ Operates on either redish.types.Set or __builtins__.set.
+
"""
- return self.client.sinter([self.name, other.name])
+ if isinstance(other, self.__class__):
+ return self.client.sinter([self.name, other.name])
+ else:
+ return self._as_set().intersection(other)
def intersection_update(self, other):
"""Update the set with the intersection of itself and another."""
@@ -191,8 +206,15 @@ def difference(self, *others):
(i.e. all elements that are in this set but not the others.)
+ Operates on either redish.types.Set or __builtins__.set.
+
"""
- return self.client.sdiff([self.name] + [other.name for other in others])
+ if all([isinstance(a, self.__class__) for a in others]):
+ return self.client.sdiff([self.name] + [other.name for other in others])
+ else:
+ othersets = filter(lambda x: isinstance(x, set), others)
+ otherTypes = filter(lambda x: isinstance(x, self.__class__), others)
+ return self.client.sdiff([self.name] + [other.name for other in otherTypes]).difference(*othersets)
def difference_update(self, other):
"""Remove all elements of another set from this set."""
@@ -320,11 +342,7 @@ def itemsview(self, start=0, end=-1, desc=False):
def keysview(self, start=0, end=-1, desc=False):
return self._itemsview(self, start, end, desc, withscores=False)
-
-
-
-
-
+ copy = _as_set
class Dict(Type):
@@ -448,6 +466,8 @@ def update(self, other):
def _as_dict(self):
return self.client.hgetall(self.name)
+ copy = _as_dict
+
class Queue(Type):
"""FIFO Queue."""
@@ -701,6 +721,7 @@ def __int__(self):
def __repr__(self):
return repr(int(self))
+ copy = __int__
def is_zsettable(s):
"""quick check that all values in a dict are reals"""
Please sign in to comment.
Something went wrong with that request. Please try again.