Commits

Matthew Frazier committed acf9076

split collection classes into read-only and read-write

  • Participants
  • Parent commits 5192161

Comments (0)

Files changed (5)

 env/*
 dist/*
 *.egg-info
-docs/_build/*
+docs/_build/*
+*.DS_Store

File redismap/__init__.py

 from .key import Key, to_key
 from .converters import (UTF8, UTF16, Bytes, Integer, Decimal, Float, Boolean,
                          DateTime, Timestamp, Date)
-from .collections import Set, List, SortedSet, Hash, Queue, Stack
+from .collections import (Set, SetView, List, ListView, SortedSet,
+                          SortedSetView, Hash, HashView, Queue, Stack)
 from .exceptions import RedismapError, ValidationError
 from .redisobject import RedisObject, Definition
 from .structure import Field, Structure

File redismap/collections/__init__.py

 :copyright: (C) 2011 Matthew Frazier
 :license:   MIT/X11, see LICENSE for details
 """
-from .native import Set, List, SortedSet, Hash
+from .native import (Set, SetView, List, ListView, SortedSet, SortedSetView,
+                     Hash, HashView)
 from .ifos import Queue, Stack

File redismap/collections/native.py

 from ..redisobject import RedisObject
 from ..utils import slice_to_lrange
 
-class Collection(RedisObject):
+class CollectionView(RedisObject):
     def __init__(self, key, converter=UTF8):
         self.key = key
         self.converter = converter
+
+
+class SetView(CollectionView):
+    """
+    This is a read-only representation of a Redis set. You can access its
+    items in all of the ways you can with a Python set (see the `Set` class
+    for a full example), but there are no methods for modifying it.
     
-    def clear(self):
+    :param key: The key that the set is stored in.
+    :param converter: A converter to store and retrieve the items of the set.
+    """
+    def _to_redis(self, value):
+        return self.converter.to_redis(value)
+    
+    def _from_redis(self, value):
+        return self.converter.from_redis(value)
+    
+    def __len__(self):
+        return self.key.scard()
+    
+    def __iter__(self):
+        return (self._from_redis(v) for v in self.key.smembers())
+    
+    def __contains__(self, item):
+        return self.key.sismember(self._to_redis(item))
+    
+    def copy(self):
         """
-        Deletes this collection, removing all of its items.
+        Returns a Python `set` with all of this set's items. Note that
+        modifying the returned `set` will *not* reflect changes back to this
+        one.
         """
-        self.key.delete()
+        return set(self)
+    
+    def randmember(self):
+        """
+        Returns a random item from the set, *without* removing it. It will
+        return `None` if no items are in the set.
+        """
+        item = self.key.srandmember()
+        if item is not None:
+            return self._from_redis(item)
 
 
-class Set(Collection):
+class Set(SetView):
     """
     This represents a Redis set. You can do most of the operations you can do
     to a Python set with it, including:
         False
     
     Most of the Python set methods are supported, except for intersections,
-    differences, and the like.
+    differences, and the like. This is a writable view on the set - all of the
+    reading methods are documented on the `SetView` class.
     
     :param key: The key that the set is stored in.
     :param converter: A converter to store and retrieve the items of the set.
     """
-    def _to_redis(self, value):
-        return self.converter.to_redis(value)
-    
-    def _from_redis(self, value):
-        return self.converter.from_redis(value)
-    
-    def __len__(self):
-        return self.key.scard()
-    
-    def __iter__(self):
-        return (self._from_redis(v) for v in self.key.smembers())
-    
-    def __contains__(self, item):
-        return self.key.sismember(self._to_redis(item))
-    
     def add(self, item):
         """
         Adds an item to the set. If the item was already in the set, `False`
         """
         return self.key.sadd(self._to_redis(item))
     
-    def copy(self):
+    def clear(self):
         """
-        Returns a Python `set` with all of this set's items. Note that
-        modifying the returned `set` will *not* reflect changes back to this
-        one.
+        Deletes the set, removing all of its items.
         """
-        return set(self)
+        self.key.delete()
     
     def discard(self, item):
         """
         item = self.key.spop()
         if item is not None:
             return self._from_redis(item)
-    
-    def randmember(self):
-        """
-        Returns a random item from the set, *without* removing it. It will
-        return `None` if no items are in the set.
-        """
-        item = self.key.srandmember()
-        if item is not None:
-            return self._from_redis(item)
 
 
-class List(Collection):
+class ListView(CollectionView):
     """
-    This represents a Redis list. You can use it like a Python `list` for most
-    operations.
+    This is a read-only representation of a Redis list. You can access data
+    from it as you would a Python list (though in this case a tuple might be
+    a better analogy), but there are no methods for modifying
+    it. (For a writable view of a list, see `List`.)
     
     :param key: The key that the list is stored in.
     :param converter: A converter to store and retrieve the items of the list.
         else:
             raise TypeError("May only index a list by int or slice")
     
-    def __setitem__(self, item, value):
-        if isinstance(item, int):
-            self.key.lset(item, self._to_redis(value))
-        else:
-            raise TypeError("May only set a list index by int")
-    
     def __iter__(self):
         return (self._from_redis(i) for i in self.key.lrange(0, -1))
     
         """
         return [self._from_redis(i)
                 for i in self.key.lrange(start, stop)]
+
+
+class List(ListView):
+    """
+    This represents a Redis list. You can use it like a Python `list` for most
+    operations. This is a writable view on the list - all of the reading
+    methods are documented in `ListView`.
+    
+    :param key: The key that the list is stored in.
+    :param converter: A converter to store and retrieve the items of the list.
+                      The default is `UTF8`.
+    """
+    def __setitem__(self, item, value):
+        if isinstance(item, int):
+            self.key.lset(item, self._to_redis(value))
+        else:
+            raise TypeError("May only set a list index by int")
+    
+    def clear(self):
+        """
+        Deletes the list, removing all of its items.
+        """
+        self.key.delete()
     
     def trim(self, start=0, stop=-1):
         """
             return self._from_redis(item)
 
 
-class SortedSet(Collection):
+class SortedSetView(CollectionView):
     """
-    This represents a Redis sorted set ("zset").
+    This is a read-only representation of a Redis sorted set ("zset").
     
     :param key: The key that the set is stored in.
     :param converter: A converter to store and retrieve the items of the set.
     
     def __contains__(self, item):
         return self.score(item) is not None
-    
-    def __setitem__(self, item, score):
-        self.add(item, score)
-    
+        
     def score(self, item):
         """
         This gets the score for the given item. If the item is not in the
         """
         return self.key.zrevrank(self._to_redis(item))
     
-    def add(self, item, score):
-        """
-        This adds an item to the sorted set, with the given score. If the item
-        already exists, its score will be replaced with the given. It will
-        return `False` if the item was already in the set and `True` if it was
-        not.
-        
-        You can also set an item's score with ``zset[item] = score``, as with
-        a dictionary.
-        
-        :param item: The item to add to the zset.
-        :param score: The score to give to the item, as a `float`.
-        """
-        score = self._to_zscore(score)
-        return self.key.zadd(self._to_redis(item), score)
-    
-    def incr(self, item, by=1.0):
-        """
-        This increases the score of an item within the sorted set. If the
-        item was already in the set, its score will be increased by the given
-        amount (1.0 is the default). If the item was not in the set, its score
-        will be set to that amount (as if its score was 0.0).
-        
-        The new score of the item will be returned. Note that if you are
-        storing values like dates and times in the zset, this method likely
-        will not make much sense.
-        
-        :param item: The item to increment the score of.
-        :param by: The amount to increment (or decrement) it by, as a `float`.
-                   The default is 1.0.
-        """
-        by = float(by)
-        return self.key.zincrby(self._to_redis(item), by)
-    
-    def remove(self, item):
-        """
-        Removes a certain item from the sorted set. If the item was in the
-        zset to begin with, it returns `True`. Otherwise, it returns `False`.
-        
-        :param item: The item to remove.
-        """
-        return self.key.zrem(self._to_redis(item))
-    
     def _rank_range(self, start, stop, reverse, withscores):
         if reverse:
             return self.key.zrevrange(start, stop, withscores=withscores)
             return [self._from_redis(i) for i in rawitems]
 
 
+class SortedSet(SortedSetView):
+    """
+    This represents a Redis sorted set ("zset"). The interface is a hybrid
+    of the interface for a Python `set` and `dict`. This is a writable view
+    on the zset - all of the reading and querying methods are covered in
+    `SortedSetView`.
+    
+    :param key: The key that the set is stored in.
+    :param converter: A converter to store and retrieve the items of the set.
+                      The default is `UTF8`.
+    :param scoreconverter: A converter to rank the items of the set. The
+                           default is `Float`.
+    """
+    def __setitem__(self, item, score):
+        self.add(item, score)
+    
+    def clear(self):
+        """
+        Deletes the sorted set, removing all of its items.
+        """
+        self.key.delete()
+    
+    def add(self, item, score):
+        """
+        This adds an item to the sorted set, with the given score. If the item
+        already exists, its score will be replaced with the given. It will
+        return `False` if the item was already in the set and `True` if it was
+        not.
+        
+        You can also set an item's score with ``zset[item] = score``, as with
+        a dictionary.
+        
+        :param item: The item to add to the zset.
+        :param score: The score to give to the item, as a `float`.
+        """
+        score = self._to_zscore(score)
+        return self.key.zadd(self._to_redis(item), score)
+    
+    def incr(self, item, by=1.0):
+        """
+        This increases the score of an item within the sorted set. If the
+        item was already in the set, its score will be increased by the given
+        amount (1.0 is the default). If the item was not in the set, its score
+        will be set to that amount (as if its score was 0.0).
+        
+        The new score of the item will be returned. Note that if you are
+        storing values like dates and times in the zset, this method likely
+        will not make much sense.
+        
+        :param item: The item to increment the score of.
+        :param by: The amount to increment (or decrement) it by, as a `float`.
+                   The default is 1.0.
+        """
+        by = float(by)
+        return self.key.zincrby(self._to_redis(item), by)
+    
+    def remove(self, item):
+        """
+        Removes a certain item from the sorted set. If the item was in the
+        zset to begin with, it returns `True`. Otherwise, it returns `False`.
+        
+        :param item: The item to remove.
+        """
+        return self.key.zrem(self._to_redis(item))
+
+
 _no_default = object()
 
-class Hash(Collection):
+class HashView(CollectionView):
+    """
+    This is a read-only representation of a Redis hash. You can access its
+    items in all of the ways you can with a Python `dict` (see the `Hash`
+    class for a full example), but there are no methods for modifying it.
+    
+    :param key: The key that the hash is stored in.
+    :param keyconverter: A converter to store and retrieve the keys of the
+                          hash. The default is `UTF8`.
+    :param valueconverter: A converter to store and retrieve the values of
+                           the hash. The default is `UTF8`.
+    """
+    def __init__(self, key, keyconverter=UTF8, valueconverter=UTF8):
+        self.key = key
+        self.keyconverter = keyconverter
+        self.valueconverter = valueconverter
+    
+    def _key_to_redis(self, value):
+        return self.keyconverter.to_redis(value)
+    
+    def _key_from_redis(self, value):
+        return self.keyconverter.from_redis(value)
+    
+    def _value_to_redis(self, value):
+        return self.valueconverter.to_redis(value)
+    
+    def _value_from_redis(self, value):
+        return self.valueconverter.from_redis(value)
+    
+    def __len__(self):
+        return self.key.hlen()
+    
+    def __getitem__(self, item):
+        item = self.key.hget(self._key_to_redis(item))
+        if item is None:
+            raise KeyError(item)
+        return self._value_from_redis(item)
+    
+    def __contains__(self, item):
+        return self.key.hexists(self._key_to_redis(item))
+    
+    def __iter__(self):
+        return self.iterkeys()
+    
+    def copy(self):
+        """
+        Returns a Python `dict` with all of this hash's items. Note that
+        modifying the returned `dict` will *not* reflect changes back to this
+        hash.
+        """
+        return dict(self.iteritems())
+    
+    def get(self, key, default=None):
+        """
+        Gets a specific item from the hash. If the item is not in the hash,
+        the `default` will be returned instead.
+        
+        :param key: The key whose value to retrieve.
+        :param default: The value to run if the given key is not in the hash.
+                        The default is `None`.
+        """
+        item = self.key.hget(self._key_to_redis(key))
+        if item is None:
+            return default
+        return self._value_from_redis(item)
+    
+    def iterkeys(self):
+        """
+        Returns an iterator over all of the keys in the hash. Their order is
+        not guaranteed.
+        """
+        return (self._key_from_redis(key)
+                for key in self.key.hkeys())
+    
+    def keys(self):
+        """
+        Returns a `list` of all the keys in the hash. Their order is not
+        guaranteed.
+        """
+        return list(self.iterkeys())
+    
+    def itervalues(self):
+        """
+        Returns an iterator over all of the values in the hash. Their order is
+        not guaranteed.
+        """
+        return (self._value_from_redis(value)
+                for value in self.key.hvals())
+    
+    def keys(self):
+        """
+        Returns a `list` of all the values in the hash. Their order is not
+        guaranteed.
+        """
+        return list(self.itervalues())
+    
+    def iteritems(self):
+        """
+        Returns an iterator over ``(key, value)`` tuples for each key-value
+        pair in the hash. Their order is not guaranteed.
+        """
+        return ((self._key_from_redis(key),
+                 self._value_from_redis(value))
+                for (key, value) in self.key.hgetall().iteritems())
+    
+    def items(self):
+        """
+        Returns a `list` of ``(key, value)`` tuples for each key-value pair
+        in the hash. Their order is not guaranteed.
+        """
+        return list(self.items())
+
+
+class Hash(HashView):
     """
     This represents a Redis hash. You can use it like a Python `dict`.
     
     :param valueconverter: A converter to store and retrieve the values of
                            the hash. The default is `UTF8`.
     """
-    def __init__(self, key, keyconverter=UTF8, valueconverter=UTF8):
-        self.key = key
-        self.keyconverter = keyconverter
-        self.valueconverter = valueconverter
-    
-    def _key_to_redis(self, value):
-        return self.keyconverter.to_redis(value)
-    
-    def _key_from_redis(self, value):
-        return self.keyconverter.from_redis(value)
-    
-    def _value_to_redis(self, value):
-        return self.valueconverter.to_redis(value)
-    
-    def _value_from_redis(self, value):
-        return self.valueconverter.from_redis(value)
-    
-    def __len__(self):
-        return self.key.hlen()
-    
-    def __getitem__(self, item):
-        item = self.key.hget(self._key_to_redis(item))
-        if item is None:
-            raise KeyError(item)
-        return self._value_from_redis(item)
-    
     def __setitem__(self, item, value):
         self.key.hset(self._key_to_redis(item),
                       self._value_to_redis(value))
     def __delitem__(self, item):
         self.key.hdel(self._key_to_redis(item))
     
-    def __contains__(self, item):
-        return self.key.hexists(self._key_to_redis(item))
-    
-    def __iter__(self):
-        return self.iterkeys()
-    
-    def copy(self):
+    def clear(self):
         """
-        Returns a Python `dict` with all of this hash's items. Note that
-        modifying the returned `dict` will *not* reflect changes back to this
-        hash.
+        Deletes the hash, removing all of its items.
         """
-        return dict(self.iteritems())
-    
-    def get(self, key, default=None):
-        """
-        Gets a specific item from the hash. If the item is not in the hash,
-        the `default` will be returned instead.
-        
-        :param key: The key whose value to retrieve.
-        :param default: The value to run if the given key is not in the hash.
-                        The default is `None`.
-        """
-        item = self.key.hget(self._key_to_redis(key))
-        if item is None:
-            return default
-        return self._value_from_redis(item)
+        self.key.delete()
     
     def incr(self, key, by=1):
         """
             if default is _no_default:
                 raise KeyError(key)
             return default
-        return self._value_from_redis(value)
-    
-    def iterkeys(self):
-        """
-        Returns an iterator over all of the keys in the hash. Their order is
-        not guaranteed.
-        """
-        return (self._key_from_redis(key)
-                for key in self.key.hkeys())
-    
-    def keys(self):
-        """
-        Returns a `list` of all the keys in the hash. Their order is not
-        guaranteed.
-        """
-        return list(self.iterkeys())
-    
-    def itervalues(self):
-        """
-        Returns an iterator over all of the values in the hash. Their order is
-        not guaranteed.
-        """
-        return (self._value_from_redis(value)
-                for value in self.key.hvals())
-    
-    def keys(self):
-        """
-        Returns a `list` of all the values in the hash. Their order is not
-        guaranteed.
-        """
-        return list(self.itervalues())
-    
-    def iteritems(self):
-        """
-        Returns an iterator over ``(key, value)`` tuples for each key-value
-        pair in the hash. Their order is not guaranteed.
-        """
-        return ((self._key_from_redis(key),
-                 self._value_from_redis(value))
-                for (key, value) in self.key.hgetall().iteritems())
-    
-    def items(self):
-        """
-        Returns a `list` of ``(key, value)`` tuples for each key-value pair
-        in the hash. Their order is not guaranteed.
-        """
-        return list(self.items())
+        return self._value_from_redis(value)

File tests/collections/native.py

 """
 from attest import Tests, raises, assert_hook
 from datetime import date
-from redismap import Key, Set, List, SortedSet, Hash, Integer, Bytes, Date
+from redismap import (Key, Set, SetView, List, ListView, SortedSet,
+                      SortedSetView, Hash, HashView, Integer, Bytes, Date)
 from .._environment import client
 
 def sort(i):
 nativetests = Tests()
 
 @nativetests.test
+def set_view():
+    k = Key("~set", client)
+    k.delete()
+    s = SetView(k, Integer)
+    # length
+    assert len(s) == 0
+    k.sadd("1")
+    k.sadd("2")
+    k.sadd("3")
+    assert len(s) == 3
+    # itering and copying
+    assert sort(s) == [1, 2, 3]
+    assert s.copy() == set([1, 2, 3])
+    # randmember
+    assert s.randmember() in (1, 2, 3)
+    k.delete()
+
+
+@nativetests.test
 def set_collection():
     s = Set(Key("~set", client), Integer)
     s.clear()
     assert len(s) == 4
     assert 2 in s
     assert 3 not in s
-    # itering and copying
-    assert sort(s) == [1, 2, 4, 5]
-    assert s.copy() == set([1, 2, 4, 5])
     # removing items
     assert s.discard(5)
     assert 5 not in s
     assert 4 not in s
     with raises(KeyError):
         s.remove(3)
-    # pop and randmember
-    item = s.randmember()
-    assert item in [1, 2]
+    # pop
     item = s.pop()
     assert item in [1, 2]
     assert item not in s
 
 
 @nativetests.test
+def list_view():
+    k = Key("~list", client)
+    k.delete()
+    l = ListView(k, Bytes)
+    # length
+    assert len(l) == 0
+    pipe = k.client.pipeline()
+    for char in "hijklmnopqrs":
+        pipe.rpush(k, char)
+    pipe.execute()
+    assert len(l) == 12
+    assert l.copy() == list("hijklmnopqrs")
+    assert tuple(l) == tuple("hijklmnopqrs")
+    # slicing and ranging
+    assert l[4:8] == ["l", "m", "n", "o"]
+    assert l.range(4, 8) == ["l", "m", "n", "o", "p"]
+    assert l[:4] == ["h", "i", "j", "k"]
+    assert l.range(0, 4) == ["h", "i", "j", "k", "l"]
+    
+
+
+@nativetests.test
 def list_collection():
     l = List(Key("~list", client), Bytes)
     l.clear()
     l.preextend("hijk")
     assert l.copy() == list("hijklmnopqrs")
     assert len(l) == 12
-    # slicing and ranging
-    assert l[4:8] == ["l", "m", "n", "o"]
-    assert l.range(4, 8) == ["l", "m", "n", "o", "p"]
-    assert l[:4] == ["h", "i", "j", "k"]
-    assert l.range(0, 4) == ["h", "i", "j", "k", "l"]
     # pop and shift
     assert l.pop() == "s"
     assert l.pop() == "r"
 
 
 @nativetests.test
+def zset_view():
+    k = Key("~zset", client)
+    k.delete()
+    z = SortedSetView(k, Bytes)
+    assert len(z) == 0
+    k.zadd("uno", 0.5)
+    k.zadd("dos", 2.0)
+    k.zadd("tres", 3.0)
+    k.zadd("cuatro", 5.0)
+    assert len(z) == 4
+    # getting scores
+    assert z["uno"] == 0.5
+    assert z.score("dos") == 2.0
+    assert z.score("tres") == 3.0
+    assert z.score("cuatro") == 5.0
+    assert z.score("seis") is None
+    with raises(KeyError):
+        z["siete"]
+    assert "cuatro" in z
+    assert "cinco" not in z
+    # rank
+    assert z.rank("uno") == 0
+    assert z.revrank("dos") == 2
+    assert z.rank("tres") == 2
+    assert z.revrank("cuatro") == 0
+    k.delete()
+
+
+@nativetests.test
 def zset_collection():
     z = SortedSet(Key("~zset", client), Bytes)
     z.clear()
     z.add("dos", 2.0)
     z["tres"] = 2.0
     z["cuatro"] = 5.0
-    # getting scores
-    assert z["uno"] == 0.5
-    assert z.score("dos") == 2.0
-    assert z.score("tres") == 2.0
-    assert z.score("cuatro") == 5.0
-    assert z.score("seis") is None
-    with raises(KeyError):
-        z["siete"]
-    assert "cuatro" in z
-    assert "cinco" not in z
     # incr
     z["uno"] = 1.0
     assert z["uno"] == 1.0
     assert z.incr("tres") == 3.0
     assert z.incr("cuatro", -1.0) == 4.0
-    # rank
-    assert z.rank("uno") == 0
-    assert z.revrank("dos") == 2
-    assert z.rank("tres") == 2
-    assert z.revrank("cuatro") == 0
     # removing
     assert not z.remove("seis")
     assert z.remove("cuatro")
 
 
 @nativetests.test
+def hash_view():
+    k = Key("~hash", client)
+    k.delete()
+    h = HashView(k, Integer, Integer)
+    assert len(h) == 0
+    k.hmset({1: 40, 2: 80, 3: 120, 4: 160})
+    # getting keys
+    assert h[1] == 40
+    assert h.get(2) == 80
+    assert 3 in h
+    assert h.get(5) is None
+    assert 6 not in h
+    with raises(KeyError):
+        h[7]
+    assert h.copy() == {1: 40, 2: 80, 3: 120, 4: 160}
+    # iterkeys/itervalues/iteritems
+    assert sort(h.iterkeys()) == [1, 2, 3, 4]
+    assert sort(h.itervalues()) == [40, 80, 120, 160]
+    assert sort(h.iteritems()) == [(1, 40), (2, 80), (3, 120), (4, 160)]
+    k.delete()
+
+
+@nativetests.test
 def hash_basics():
     h = Hash(Key("~hash", client), Integer, Integer)
     h.clear()
     assert client.hget("~hash", 1) == "40"
     assert client.hget("~hash", 2) == "80"
     h[3] = 100
-    # getting keys
-    assert h[1] == 40
-    assert h.get(2) == 80
-    assert 3 in h
-    assert h.get(4) is None
-    assert 5 not in h
-    with raises(KeyError):
-        h[6]
-    h[3] = 120
-    assert h[3] == 120
-    assert h.copy() == {1: 40, 2: 80, 3: 120}
     # setdefault
-    assert h.setdefault(3, 100) == 120
-    assert h[3] == 120
+    assert h.setdefault(3, 100) == 100
+    assert h[3] == 100
     assert h.setdefault(4, 150) == 150
     assert h[4] == 150
     # update
     assert h.pop(5, None) is None
     with raises(KeyError):
         h.pop(6)
-    # iterkeys/itervalues/iteritems
-    assert sort(h.iterkeys()) == [1, 2, 3, 4]
-    assert sort(h.itervalues()) == [40, 80, 120, 160]
-    assert sort(h.iteritems()) == [(1, 40), (2, 80), (3, 120), (4, 160)]
-    h.clear()
+    h.clear()