Code

forgot to fix the templating for last change
[roundup.git] / roundup / hyperdb.py
index 15f5c80d217a47d666d6c61348c81ed9deda0ac5..5761e1d872ece31fda8fbd6bd86e3b6260e8a679 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: hyperdb.py,v 1.50 2002-01-19 13:16:04 rochecompaan Exp $
+# $Id: hyperdb.py,v 1.64 2002-05-15 06:21:21 richard Exp $
 
 __doc__ = """
 Hyperdatabase implementation, especially field types.
 """
 
 # standard python modules
-import cPickle, re, string, weakref
+import re, string, weakref, os, time
 
 # roundup modules
 import date, password
 
+# configure up the DEBUG and TRACE captures
+class Sink:
+    def write(self, content):
+        pass
+DEBUG = os.environ.get('HYPERDBDEBUG', '')
+if DEBUG and __debug__:
+    DEBUG = open(DEBUG, 'a')
+else:
+    DEBUG = Sink()
+TRACE = os.environ.get('HYPERDBTRACE', '')
+if TRACE and __debug__:
+    TRACE = open(TRACE, 'w')
+else:
+    TRACE = Sink()
+def traceMark():
+    print >>TRACE, '**MARK', time.ctime()
+del Sink
 
 #
 # Types
@@ -34,43 +51,60 @@ import date, password
 class String:
     """An object designating a String property."""
     def __repr__(self):
+        ' more useful for dumps '
         return '<%s>'%self.__class__
 
 class Password:
     """An object designating a Password property."""
     def __repr__(self):
+        ' more useful for dumps '
         return '<%s>'%self.__class__
 
 class Date:
     """An object designating a Date property."""
     def __repr__(self):
+        ' more useful for dumps '
         return '<%s>'%self.__class__
 
 class Interval:
     """An object designating an Interval property."""
     def __repr__(self):
+        ' more useful for dumps '
         return '<%s>'%self.__class__
 
 class Link:
     """An object designating a Link property that links to a
        node in a specified class."""
     def __init__(self, classname, do_journal='no'):
+        ''' Default is to not journal link and unlink events
+        '''
         self.classname = classname
         self.do_journal = do_journal == 'yes'
     def __repr__(self):
+        ' more useful for dumps '
         return '<%s to "%s">'%(self.__class__, self.classname)
 
 class Multilink:
     """An object designating a Multilink property that links
        to nodes in a specified class.
+
+       "classname" indicates the class to link to
+
+       "do_journal" indicates whether the linked-to nodes should have
+                    'link' and 'unlink' events placed in their journal
     """
     def __init__(self, classname, do_journal='no'):
+        ''' Default is to not journal link and unlink events
+        '''
         self.classname = classname
         self.do_journal = do_journal == 'yes'
     def __repr__(self):
+        ' more useful for dumps '
         return '<%s to "%s">'%(self.__class__, self.classname)
 
 class DatabaseError(ValueError):
+    '''Error to be raised when there is some problem in the database code
+    '''
     pass
 
 
@@ -153,11 +187,68 @@ transaction.
         '''
         raise NotImplementedError
 
+    def serialise(self, classname, node):
+        '''Copy the node contents, converting non-marshallable data into
+           marshallable data.
+        '''
+        if __debug__:
+            print >>DEBUG, 'serialise', classname, node
+        properties = self.getclass(classname).getprops()
+        d = {}
+        for k, v in node.items():
+            # if the property doesn't exist, or is the "retired" flag then
+            # it won't be in the properties dict
+            if not properties.has_key(k):
+                d[k] = v
+                continue
+
+            # get the property spec
+            prop = properties[k]
+
+            if isinstance(prop, Password):
+                d[k] = str(v)
+            elif isinstance(prop, Date) and v is not None:
+                d[k] = v.get_tuple()
+            elif isinstance(prop, Interval) and v is not None:
+                d[k] = v.get_tuple()
+            else:
+                d[k] = v
+        return d
+
     def setnode(self, classname, nodeid, node):
         '''Change the specified node.
         '''
         raise NotImplementedError
 
+    def unserialise(self, classname, node):
+        '''Decode the marshalled node data
+        '''
+        if __debug__:
+            print >>DEBUG, 'unserialise', classname, node
+        properties = self.getclass(classname).getprops()
+        d = {}
+        for k, v in node.items():
+            # if the property doesn't exist, or is the "retired" flag then
+            # it won't be in the properties dict
+            if not properties.has_key(k):
+                d[k] = v
+                continue
+
+            # get the property spec
+            prop = properties[k]
+
+            if isinstance(prop, Date) and v is not None:
+                d[k] = date.Date(v)
+            elif isinstance(prop, Interval) and v is not None:
+                d[k] = date.Interval(v)
+            elif isinstance(prop, Password):
+                p = password.Password()
+                p.unpack(v)
+                d[k] = p
+            else:
+                d[k] = v
+        return d
+
     def getnode(self, classname, nodeid, db=None, cache=1):
         '''Get a node from the database.
         '''
@@ -206,6 +297,11 @@ transaction.
         '''
         raise NotImplementedError
 
+    def pack(self, pack_before):
+        ''' pack the database
+        '''
+        raise NotImplementedError
+
     def commit(self):
         ''' Commit the current transactions.
 
@@ -245,6 +341,8 @@ class Class:
         db.addclass(self)
 
     def __repr__(self):
+        '''Slightly more useful representation
+        '''
         return '<hypderdb.Class "%s">'%self.classname
 
     # Editing nodes:
@@ -273,7 +371,7 @@ class Class:
             raise DatabaseError, 'Database open read-only'
 
         # new node's id
-        newid = str(self.count() + 1)
+        newid = self.db.newid(self.classname)
 
         # validate propvalues
         num_re = re.compile('^\d+$')
@@ -371,17 +469,6 @@ class Class:
                 # TODO: None isn't right here, I think...
                 propvalues[key] = None
 
-        # convert all data to strings
-        for key, prop in self.properties.items():
-            if isinstance(prop, Date):
-                if propvalues[key] is not None:
-                    propvalues[key] = propvalues[key].get_tuple()
-            elif isinstance(prop, Interval):
-                if propvalues[key] is not None:
-                    propvalues[key] = propvalues[key].get_tuple()
-            elif isinstance(prop, Password):
-                propvalues[key] = str(propvalues[key])
-
         # done
         self.db.addnode(self.classname, newid, propvalues)
         self.db.addjournal(self.classname, newid, 'create', propvalues)
@@ -418,20 +505,6 @@ class Class:
             else:
                 return default
 
-        # possibly convert the marshalled data to instances
-        if isinstance(prop, Date):
-            if d[propname] is None:
-                return None
-            return date.Date(d[propname])
-        elif isinstance(prop, Interval):
-            if d[propname] is None:
-                return None
-            return date.Interval(d[propname])
-        elif isinstance(prop, Password):
-            p = password.Password()
-            p.unpack(d[propname])
-            return p
-
         return d[propname]
 
     # XXX not in spec
@@ -494,6 +567,13 @@ class Class:
             # the writeable properties.
             prop = self.properties[key]
 
+            # if the value's the same as the existing value, no sense in
+            # doing anything
+            if node.has_key(key) and value == node[key]:
+                del propvalues[key]
+                continue
+
+            # do stuff based on the prop type
             if isinstance(prop, Link):
                 link_class = self.properties[key].classname
                 # if it isn't a number, it's a key
@@ -573,20 +653,25 @@ class Class:
             elif isinstance(prop, Password):
                 if not isinstance(value, password.Password):
                     raise TypeError, 'new property "%s" not a Password'% key
-                propvalues[key] = value = str(value)
+                propvalues[key] = value
 
             elif value is not None and isinstance(prop, Date):
                 if not isinstance(value, date.Date):
                     raise TypeError, 'new property "%s" not a Date'% key
-                propvalues[key] = value = value.get_tuple()
+                propvalues[key] = value
 
             elif value is not None and isinstance(prop, Interval):
                 if not isinstance(value, date.Interval):
                     raise TypeError, 'new property "%s" not an Interval'% key
-                propvalues[key] = value = value.get_tuple()
+                propvalues[key] = value
 
             node[key] = value
 
+        # nothing to do?
+        if not propvalues:
+            return
+
+        # do the set, and journal it
         self.db.setnode(self.classname, nodeid, node)
         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
 
@@ -622,6 +707,10 @@ class Class:
         return self.db.getjournal(self.classname, nodeid)
 
     # Locating nodes:
+    def hasnode(self, nodeid):
+        '''Determine if the given nodeid actually exists
+        '''
+        return self.db.hasnode(self.classname, nodeid)
 
     def setkey(self, propname):
         """Select a String property of this class to be the key property.
@@ -835,7 +924,7 @@ class Class:
                     else:
                         continue
                     break
-                elif t == 2 and not v.search(node[k]):
+                elif t == 2 and (node[k] is None or not v.search(node[k])):
                     # RE search
                     break
                 elif t == 6 and node[k] != v:
@@ -998,7 +1087,6 @@ class Class:
                 raise ValueError, key
         self.properties.update(properties)
 
-
 # XXX not in spec
 class Node:
     ''' A convenience wrapper for the given node
@@ -1047,14 +1135,77 @@ class Node:
         return self.cl.retire(self.nodeid)
 
 
-def Choice(name, *options):
-    cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
+def Choice(name, db, *options):
+    '''Quick helper to create a simple class with choices
+    '''
+    cl = Class(db, name, name=String(), order=String())
     for i in range(len(options)):
-        cl.create(name=option[i], order=i)
+        cl.create(name=options[i], order=i)
     return hyperdb.Link(name)
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.63  2002/04/15 23:25:15  richard
+# . node ids are now generated from a lockable store - no more race conditions
+#
+# We're using the portalocker code by Jonathan Feinberg that was contributed
+# to the ASPN Python cookbook. This gives us locking across Unix and Windows.
+#
+# Revision 1.62  2002/04/03 07:05:50  richard
+# d'oh! killed retirement of nodes :(
+# all better now...
+#
+# Revision 1.61  2002/04/03 06:11:51  richard
+# Fix for old databases that contain properties that don't exist any more.
+#
+# Revision 1.60  2002/04/03 05:54:31  richard
+# Fixed serialisation problem by moving the serialisation step out of the
+# hyperdb.Class (get, set) into the hyperdb.Database.
+#
+# Also fixed htmltemplate after the showid changes I made yesterday.
+#
+# Unit tests for all of the above written.
+#
+# Revision 1.59  2002/03/12 22:52:26  richard
+# more pychecker warnings removed
+#
+# Revision 1.58  2002/02/27 03:23:16  richard
+# Ran it through pychecker, made fixes
+#
+# Revision 1.57  2002/02/20 05:23:24  richard
+# Didn't accomodate new values for new properties
+#
+# Revision 1.56  2002/02/20 05:05:28  richard
+#  . Added simple editing for classes that don't define a templated interface.
+#    - access using the admin "class list" interface
+#    - limited to admin-only
+#    - requires the csv module from object-craft (url given if it's missing)
+#
+# Revision 1.55  2002/02/15 07:27:12  richard
+# Oops, precedences around the way w0rng.
+#
+# Revision 1.54  2002/02/15 07:08:44  richard
+#  . Alternate email addresses are now available for users. See the MIGRATION
+#    file for info on how to activate the feature.
+#
+# Revision 1.53  2002/01/22 07:21:13  richard
+# . fixed back_bsddb so it passed the journal tests
+#
+# ... it didn't seem happy using the back_anydbm _open method, which is odd.
+# Yet another occurrance of whichdb not being able to recognise older bsddb
+# databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
+# process.
+#
+# Revision 1.52  2002/01/21 16:33:19  rochecompaan
+# You can now use the roundup-admin tool to pack the database
+#
+# Revision 1.51  2002/01/21 03:01:29  richard
+# brief docco on the do_journal argument
+#
+# Revision 1.50  2002/01/19 13:16:04  rochecompaan
+# Journal entries for link and multilink properties can now be switched on
+# or off.
+#
 # Revision 1.49  2002/01/16 07:02:57  richard
 #  . lots of date/interval related changes:
 #    - more relaxed date format for input