Extended libbe.storage.base for separate read/write control.
authorW. Trevor King <wking@drexel.edu>
Tue, 8 Dec 2009 08:52:37 +0000 (03:52 -0500)
committerW. Trevor King <wking@drexel.edu>
Tue, 8 Dec 2009 08:52:37 +0000 (03:52 -0500)
Rather than just having .read_only to set write permissions and
assuming that read was always legal.  We also added user and backend
control of both readable and writeable:
  do you want to read/write?
and
  can you read/write?

Specialized NotSupported into NotWriteable and NotReadable.

Added automatic unicode encoding on .set(), and decode option on
.get().

libbe/storage/base.py

index 002cc0f08a012ab93e015f986723f114a5ef16c6..352646259d238e772dae861e5e164df7f11502a4 100644 (file)
@@ -7,6 +7,7 @@ Abstract bug repository data storage to easily support multiple backends.
 import copy
 import os
 import pickle
+import types
 
 from libbe.error import NotSupported
 from libbe.util.tree import Tree
@@ -30,6 +31,14 @@ class InvalidID (KeyError):
 class InvalidRevision (KeyError):
     pass
 
+class NotWriteable (NotSupported):
+    def __init__(self, msg):
+        NotSupported.__init__('write', msg)
+
+class NotReadable (NotSupported):
+    def __init__(self, msg):
+        NotSupported.__init__('read', msg)
+
 class EmptyCommit(Exception):
     def __init__(self):
         Exception.__init__(self, 'No changes to commit')
@@ -92,10 +101,14 @@ class Storage (object):
     """
     name = 'Storage'
 
-    def __init__(self, repo, options=None):
+    def __init__(self, repo, encoding='utf-8', options=None):
         self.repo = repo
+        self.encoding = encoding
         self.options = options
-        self.read_only = False
+        self.readable = True  # soft limit (user choice)
+        self._readable = True # hard limit (backend choice)
+        self.writeable = True  # soft limit (user choice)
+        self._writeable = True # hard limit (backend choice)
         self.versioned = False
         self.can_init = True
 
@@ -109,13 +122,19 @@ class Storage (object):
         """Return a version string for this backend."""
         return '0'
 
+    def is_readable(self):
+        return self.readable and self._readable
+
+    def is_writeable(self):
+        return self.writeable and self._writeable
+
     def init(self):
         """Create a new storage repository."""
         if self.can_init == False:
             raise NotSupported('init',
                                'Cannot initialize this repository format.')
-        if self.read_only == True:
-            raise NotSupported('init', 'Cannot initialize read only storage.')
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot initialize unwriteable storage.')
         return self._init()
 
     def _init(self):
@@ -127,8 +146,8 @@ class Storage (object):
 
     def destroy(self):
         """Remove the storage repository."""
-        if self.read_only == True:
-            raise NotSupported('destroy', 'Cannot destroy read only storage.')
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot destroy unwriteable storage.')
         return self._destroy()
 
     def _destroy(self):
@@ -136,6 +155,11 @@ class Storage (object):
 
     def connect(self):
         """Open a connection to the repository."""
+        if self.is_readable() == False:
+            raise NotReadable('Cannot connect to unreadable storage.')
+        self._connect()
+
+    def _connect(self):
         try:
             f = open(self.repo, 'rb')
         except IOError:
@@ -146,7 +170,7 @@ class Storage (object):
 
     def disconnect(self):
         """Close the connection to the repository."""
-        if self.read_only == True:
+        if self.is_writeable() == False:
             return
         f = open(self.repo, 'wb')
         pickle.dump(dict((k,v._objects_to_ids())
@@ -156,8 +180,8 @@ class Storage (object):
 
     def add(self, *args, **kwargs):
         """Add an entry"""
-        if self.read_only == True:
-            raise NotSupported('add', 'Cannot add entry to read only storage.')
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot add entry to unwriteable storage.')
         self._add(*args, **kwargs)
 
     def _add(self, id, parent=None):
@@ -168,9 +192,9 @@ class Storage (object):
 
     def remove(self, *args, **kwargs):
         """Remove an entry."""
-        if self.read_only == True:
-            raise NotSupported('remove',
-                               'Cannot remove entry from read only storage.')
+        if self.is_writeable() == False:
+            raise NotSupported('write',
+                               'Cannot remove entry from unwriteable storage.')
         self._remove(*args, **kwargs)
 
     def _remove(self, id):
@@ -179,22 +203,27 @@ class Storage (object):
 
     def recursive_remove(self, *args, **kwargs):
         """Remove an entry and all its decendents."""
-        if self.read_only == True:
-            raise NotSupported('recursive_remove',
-                               'Cannot remove entries from read only storage.')
+        if self.is_writeable() == False:
+            raise NotSupported('write',
+                               'Cannot remove entries from unwriteable storage.')
         self._recursive_remove(*args, **kwargs)
 
     def _recursive_remove(self, id):
         for entry in self._data[id].traverse():
             self._remove(entry.id)
 
-    def children(self, id=None, revision=None):
+    def children(self, *args, **kwargs):
         """Return a list of specified entry's children's ids."""
+        if self.is_readable() == False:
+            raise NotReadable('Cannot list children with unreadable storage.')
+        return self._children(*args, **kwargs)
+
+    def _children(self, id=None, revision=None):
         if id == None:
             id = '__ROOT__'
         return [c.id for c in self._data[id] if not c.id.startswith('__')]
 
-    def get(self, id, default=InvalidObject, revision=None):
+    def get(self, *args, **kwargs):
         """
         Get contents of and entry as they were in a given revision.
         revision==None specifies the current revision.
@@ -202,19 +231,33 @@ class Storage (object):
         If there is no id, return default, unless default is not
         given, in which case raise InvalidID.
         """
+        if self.is_readable() == False:
+            raise NotReadable('Cannot get entry with unreadable storage.')
+        if 'decode' in kwargs:
+            decode = kwargs.pop('decode')
+        else:
+            decode = False
+        value = self._get(*args, **kwargs)
+        if decode == True:
+            return unicode(value, self.encoding)
+        return value
+
+    def _get(self, id, default=InvalidObject, revision=None):
         if id in self._data:
             return self._data[id].value
         elif default == InvalidObject:
             raise InvalidID(id)
         return default
 
-    def set(self, *args, **kwargs):
+    def set(self, id, value, *args, **kwargs):
         """
         Set the entry contents.
         """
-        if self.read_only == True:
-            raise NotSupported('set', 'Cannot set entry in read only storage.')
-        self._set(*args, **kwargs)
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot set entry in unwriteable storage.')
+        if type(value) == types.UnicodeType:
+            value = value.encode(self.encoding)
+        self._set(id, value, *args, **kwargs)
 
     def _set(self, id, value):
         if id not in self._data:
@@ -244,8 +287,7 @@ class VersionedStorage (Storage):
         pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree]
         f.close()
 
-    def connect(self):
-        """Open a connection to the repository."""
+    def _connect(self):
         try:
             f = open(self.repo, 'rb')
         except IOError:
@@ -257,7 +299,7 @@ class VersionedStorage (Storage):
 
     def disconnect(self):
         """Close the connection to the repository."""
-        if self.read_only == True:
+        if self.is_writeable() == False:
             return
         f = open(self.repo, 'wb')
         pickle.dump([dict((k,v._objects_to_ids())
@@ -279,8 +321,7 @@ class VersionedStorage (Storage):
         for entry in self._data[-1][id].traverse():
             self._remove(entry.id)
 
-    def children(self, id=None, revision=None):
-        """Return a list of specified entry's children's ids."""
+    def _children(self, id=None, revision=None):
         if id == None:
             id = '__ROOT__'
         if revision == None:
@@ -288,14 +329,7 @@ class VersionedStorage (Storage):
         return [c.id for c in self._data[revision][id]
                 if not c.id.startswith('__')]
 
-    def get(self, id, default=InvalidObject, revision=None):
-        """
-        Get contents of and entry as they were in a given revision.
-        revision==None specifies the current revision.
-
-        If there is no id, return default, unless default is not
-        given, in which case raise InvalidID.
-        """
+    def _get(self, id, default=InvalidObject, revision=None):
         if revision == None:
             revision = -1
         if id in self._data[revision]:
@@ -317,8 +351,8 @@ class VersionedStorage (Storage):
         If allow_empty == False (the default), raise EmptyCommit if
         there are no changes to commit.
         """
-        if self.read_only == True:
-            raise NotSupported('commit', 'Cannot commit to read only storage.')
+        if self.is_writeable() == False:
+            raise NotWriteable('Cannot commit to unwriteable storage.')
         return self._commit(*args, **kwargs)
 
     def _commit(self, summary, body=None, allow_empty=False):
@@ -531,6 +565,30 @@ if TESTING == True:
                     "%s.get() returned %s not %s"
                     % (vars(self.Class)['name'], ret, self.val))
 
+        def test_unicode_set(self):
+            """
+            Set should define the value returned by get.
+            """
+            val = u'Fran\xe7ois'
+            self.s.add(self.id)
+            self.s.set(self.id, val)
+            ret = self.s.get(self.id, decode=True)
+            self.failUnless(type(ret) == types.UnicodeType,
+                    "%s.get() returned %s not UnicodeType"
+                    % (vars(self.Class)['name'], type(ret)))
+            self.failUnless(ret == val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], ret, self.val))
+            ret = self.s.get(self.id)
+            self.failUnless(type(ret) == types.StringType,
+                    "%s.get() returned %s not StringType"
+                    % (vars(self.Class)['name'], type(ret)))
+            s = unicode(ret, self.s.encoding)
+            self.failUnless(s == val,
+                    "%s.get() returned %s not %s"
+                    % (vars(self.Class)['name'], s, self.val))
+            
+
     class Storage_persistence_TestCase (StorageTestCase):
         """Test cases for Storage.disconnect and .connect methods."""