X-Git-Url: https://git.tokkee.org/?a=blobdiff_plain;f=roundup%2Fhyperdb.py;h=5761e1d872ece31fda8fbd6bd86e3b6260e8a679;hb=1941cfdb27feaa4e680a155d978b7cbe102a0dc5;hp=15f5c80d217a47d666d6c61348c81ed9deda0ac5;hpb=1057f0ce9f77c0eac42161add23e693fe1c13388;p=roundup.git diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py index 15f5c80..5761e1d 100644 --- a/roundup/hyperdb.py +++ b/roundup/hyperdb.py @@ -15,18 +15,35 @@ # 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 ''%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