Code

Added vim command to all source so that we don't get no steenkin' tabs :)
[roundup.git] / roundup / hyperdb.py
1 # $Id: hyperdb.py,v 1.7 2001-07-29 07:01:39 richard Exp $
3 # standard python modules
4 import cPickle, re, string
6 # roundup modules
7 import date
10 #
11 # Types
12 #
13 class BaseType:
14     isStringType = 0
15     isDateType = 0
16     isIntervalType = 0
17     isLinkType = 0
18     isMultilinkType = 0
20 class String(BaseType):
21     def __init__(self):
22         """An object designating a String property."""
23         pass
24     def __repr__(self):
25         return '<%s>'%self.__class__
26     isStringType = 1
28 class Date(BaseType, String):
29     isDateType = 1
31 class Interval(BaseType, String):
32     isIntervalType = 1
34 class Link(BaseType):
35     def __init__(self, classname):
36         """An object designating a Link property that links to
37         nodes in a specified class."""
38         self.classname = classname
39     def __repr__(self):
40         return '<%s to "%s">'%(self.__class__, self.classname)
41     isLinkType = 1
43 class Multilink(BaseType, Link):
44     """An object designating a Multilink property that links
45        to nodes in a specified class.
46     """
47     isMultilinkType = 1
49 class DatabaseError(ValueError):
50     pass
53 #
54 # the base Database class
55 #
56 class Database:
57     # flag to set on retired entries
58     RETIRED_FLAG = '__hyperdb_retired'
61 #
62 # The base Class class
63 #
64 class Class:
65     """The handle to a particular class of nodes in a hyperdatabase."""
67     def __init__(self, db, classname, **properties):
68         """Create a new class with a given name and property specification.
70         'classname' must not collide with the name of an existing class,
71         or a ValueError is raised.  The keyword arguments in 'properties'
72         must map names to property objects, or a TypeError is raised.
73         """
74         self.classname = classname
75         self.properties = properties
76         self.db = db
77         self.key = ''
79         # do the db-related init stuff
80         db.addclass(self)
82     # Editing nodes:
84     def create(self, **propvalues):
85         """Create a new node of this class and return its id.
87         The keyword arguments in 'propvalues' map property names to values.
89         The values of arguments must be acceptable for the types of their
90         corresponding properties or a TypeError is raised.
91         
92         If this class has a key property, it must be present and its value
93         must not collide with other key strings or a ValueError is raised.
94         
95         Any other properties on this class that are missing from the
96         'propvalues' dictionary are set to None.
97         
98         If an id in a link or multilink property does not refer to a valid
99         node, an IndexError is raised.
100         """
101         if propvalues.has_key('id'):
102             raise KeyError, '"id" is reserved'
104         if self.db.journaltag is None:
105             raise DatabaseError, 'Database open read-only'
107         # new node's id
108         newid = str(self.count() + 1)
110         # validate propvalues
111         num_re = re.compile('^\d+$')
112         for key, value in propvalues.items():
113             if key == self.key:
114                 try:
115                     self.lookup(value)
116                 except KeyError:
117                     pass
118                 else:
119                     raise ValueError, 'node with key "%s" exists'%value
121             prop = self.properties[key]
123             if prop.isLinkType:
124                 if type(value) != type(''):
125                     raise ValueError, 'link value must be String'
126 #                value = str(value)
127                 link_class = self.properties[key].classname
128                 # if it isn't a number, it's a key
129                 if not num_re.match(value):
130                     try:
131                         value = self.db.classes[link_class].lookup(value)
132                     except:
133                         raise IndexError, 'new property "%s": %s not a %s'%(
134                             key, value, self.properties[key].classname)
135                 propvalues[key] = value
136                 if not self.db.hasnode(link_class, value):
137                     raise IndexError, '%s has no node %s'%(link_class, value)
139                 # register the link with the newly linked node
140                 self.db.addjournal(link_class, value, 'link',
141                     (self.classname, newid, key))
143             elif prop.isMultilinkType:
144                 if type(value) != type([]):
145                     raise TypeError, 'new property "%s" not a list of ids'%key
146                 link_class = self.properties[key].classname
147                 l = []
148                 for entry in value:
149                     if type(entry) != type(''):
150                         raise ValueError, 'link value must be String'
151                     # if it isn't a number, it's a key
152                     if not num_re.match(entry):
153                         try:
154                             entry = self.db.classes[link_class].lookup(entry)
155                         except:
156                             raise IndexError, 'new property "%s": %s not a %s'%(
157                                 key, entry, self.properties[key].classname)
158                     l.append(entry)
159                 value = l
160                 propvalues[key] = value
162                 # handle additions
163                 for id in value:
164                     if not self.db.hasnode(link_class, id):
165                         raise IndexError, '%s has no node %s'%(link_class, id)
166                     # register the link with the newly linked node
167                     self.db.addjournal(link_class, id, 'link',
168                         (self.classname, newid, key))
170             elif prop.isStringType:
171                 if type(value) != type(''):
172                     raise TypeError, 'new property "%s" not a string'%key
174             elif prop.isDateType:
175                 if not hasattr(value, 'isDate'):
176                     raise TypeError, 'new property "%s" not a Date'% key
178             elif prop.isIntervalType:
179                 if not hasattr(value, 'isInterval'):
180                     raise TypeError, 'new property "%s" not an Interval'% key
182         for key, prop in self.properties.items():
183             if propvalues.has_key(key):
184                 continue
185             if prop.isMultilinkType:
186                 propvalues[key] = []
187             else:
188                 propvalues[key] = None
190         # done
191         self.db.addnode(self.classname, newid, propvalues)
192         self.db.addjournal(self.classname, newid, 'create', propvalues)
193         return newid
195     def get(self, nodeid, propname):
196         """Get the value of a property on an existing node of this class.
198         'nodeid' must be the id of an existing node of this class or an
199         IndexError is raised.  'propname' must be the name of a property
200         of this class or a KeyError is raised.
201         """
202         if propname == 'id':
203             return nodeid
204 #        nodeid = str(nodeid)
205         d = self.db.getnode(self.classname, nodeid)
206         return d[propname]
208     # XXX not in spec
209     def getnode(self, nodeid):
210         ''' Return a convenience wrapper for the node
211         '''
212         return Node(self, nodeid)
214     def set(self, nodeid, **propvalues):
215         """Modify a property on an existing node of this class.
216         
217         'nodeid' must be the id of an existing node of this class or an
218         IndexError is raised.
220         Each key in 'propvalues' must be the name of a property of this
221         class or a KeyError is raised.
223         All values in 'propvalues' must be acceptable types for their
224         corresponding properties or a TypeError is raised.
226         If the value of the key property is set, it must not collide with
227         other key strings or a ValueError is raised.
229         If the value of a Link or Multilink property contains an invalid
230         node id, a ValueError is raised.
231         """
232         if not propvalues:
233             return
235         if propvalues.has_key('id'):
236             raise KeyError, '"id" is reserved'
238         if self.db.journaltag is None:
239             raise DatabaseError, 'Database open read-only'
241 #        nodeid = str(nodeid)
242         node = self.db.getnode(self.classname, nodeid)
243         if node.has_key(self.db.RETIRED_FLAG):
244             raise IndexError
245         num_re = re.compile('^\d+$')
246         for key, value in propvalues.items():
247             if not node.has_key(key):
248                 raise KeyError, key
250             if key == self.key:
251                 try:
252                     self.lookup(value)
253                 except KeyError:
254                     pass
255                 else:
256                     raise ValueError, 'node with key "%s" exists'%value
258             prop = self.properties[key]
260             if prop.isLinkType:
261 #                value = str(value)
262                 link_class = self.properties[key].classname
263                 # if it isn't a number, it's a key
264                 if type(value) != type(''):
265                     raise ValueError, 'link value must be String'
266                 if not num_re.match(value):
267                     try:
268                         value = self.db.classes[link_class].lookup(value)
269                     except:
270                         raise IndexError, 'new property "%s": %s not a %s'%(
271                             key, value, self.properties[key].classname)
273                 if not self.db.hasnode(link_class, value):
274                     raise IndexError, '%s has no node %s'%(link_class, value)
276                 # register the unlink with the old linked node
277                 if node[key] is not None:
278                     self.db.addjournal(link_class, node[key], 'unlink',
279                         (self.classname, nodeid, key))
281                 # register the link with the newly linked node
282                 if value is not None:
283                     self.db.addjournal(link_class, value, 'link',
284                         (self.classname, nodeid, key))
286             elif prop.isMultilinkType:
287                 if type(value) != type([]):
288                     raise TypeError, 'new property "%s" not a list of ids'%key
289                 link_class = self.properties[key].classname
290                 l = []
291                 for entry in value:
292                     # if it isn't a number, it's a key
293                     if type(entry) != type(''):
294                         raise ValueError, 'link value must be String'
295                     if not num_re.match(entry):
296                         try:
297                             entry = self.db.classes[link_class].lookup(entry)
298                         except:
299                             raise IndexError, 'new property "%s": %s not a %s'%(
300                                 key, entry, self.properties[key].classname)
301                     l.append(entry)
302                 value = l
303                 propvalues[key] = value
305                 #handle removals
306                 l = node[key]
307                 for id in l[:]:
308                     if id in value:
309                         continue
310                     # register the unlink with the old linked node
311                     self.db.addjournal(link_class, id, 'unlink',
312                         (self.classname, nodeid, key))
313                     l.remove(id)
315                 # handle additions
316                 for id in value:
317                     if not self.db.hasnode(link_class, id):
318                         raise IndexError, '%s has no node %s'%(link_class, id)
319                     if id in l:
320                         continue
321                     # register the link with the newly linked node
322                     self.db.addjournal(link_class, id, 'link',
323                         (self.classname, nodeid, key))
324                     l.append(id)
326             elif prop.isStringType:
327                 if value is not None and type(value) != type(''):
328                     raise TypeError, 'new property "%s" not a string'%key
330             elif prop.isDateType:
331                 if not hasattr(value, 'isDate'):
332                     raise TypeError, 'new property "%s" not a Date'% key
334             elif prop.isIntervalType:
335                 if not hasattr(value, 'isInterval'):
336                     raise TypeError, 'new property "%s" not an Interval'% key
338             node[key] = value
340         self.db.setnode(self.classname, nodeid, node)
341         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
343     def retire(self, nodeid):
344         """Retire a node.
345         
346         The properties on the node remain available from the get() method,
347         and the node's id is never reused.
348         
349         Retired nodes are not returned by the find(), list(), or lookup()
350         methods, and other nodes may reuse the values of their key properties.
351         """
352 #        nodeid = str(nodeid)
353         if self.db.journaltag is None:
354             raise DatabaseError, 'Database open read-only'
355         node = self.db.getnode(self.classname, nodeid)
356         node[self.db.RETIRED_FLAG] = 1
357         self.db.setnode(self.classname, nodeid, node)
358         self.db.addjournal(self.classname, nodeid, 'retired', None)
360     def history(self, nodeid):
361         """Retrieve the journal of edits on a particular node.
363         'nodeid' must be the id of an existing node of this class or an
364         IndexError is raised.
366         The returned list contains tuples of the form
368             (date, tag, action, params)
370         'date' is a Timestamp object specifying the time of the change and
371         'tag' is the journaltag specified when the database was opened.
372         """
373         return self.db.getjournal(self.classname, nodeid)
375     # Locating nodes:
377     def setkey(self, propname):
378         """Select a String property of this class to be the key property.
380         'propname' must be the name of a String property of this class or
381         None, or a TypeError is raised.  The values of the key property on
382         all existing nodes must be unique or a ValueError is raised.
383         """
384         self.key = propname
386     def getkey(self):
387         """Return the name of the key property for this class or None."""
388         return self.key
390     def labelprop(self, nodeid):
391         ''' Return the property name for a label for the given node.
393         This method attempts to generate a consistent label for the node.
394         It tries the following in order:
395             1. key property
396             2. "name" property
397             3. "title" property
398             4. first property from the sorted property name list
399         '''
400         k = self.getkey()
401         if  k:
402             return k
403         props = self.getprops()
404         if props.has_key('name'):
405             return 'name'
406         elif props.has_key('title'):
407             return 'title'
408         props = props.keys()
409         props.sort()
410         return props[0]
412     # TODO: set up a separate index db file for this? profile?
413     def lookup(self, keyvalue):
414         """Locate a particular node by its key property and return its id.
416         If this class has no key property, a TypeError is raised.  If the
417         'keyvalue' matches one of the values for the key property among
418         the nodes in this class, the matching node's id is returned;
419         otherwise a KeyError is raised.
420         """
421         cldb = self.db.getclassdb(self.classname)
422         for nodeid in self.db.getnodeids(self.classname, cldb):
423             node = self.db.getnode(self.classname, nodeid, cldb)
424             if node.has_key(self.db.RETIRED_FLAG):
425                 continue
426             if node[self.key] == keyvalue:
427                 return nodeid
428         cldb.close()
429         raise KeyError, keyvalue
431     # XXX: change from spec - allows multiple props to match
432     def find(self, **propspec):
433         """Get the ids of nodes in this class which link to a given node.
435         'propspec' consists of keyword args propname=nodeid   
436           'propname' must be the name of a property in this class, or a
437             KeyError is raised.  That property must be a Link or Multilink
438             property, or a TypeError is raised.
440           'nodeid' must be the id of an existing node in the class linked
441             to by the given property, or an IndexError is raised.
442         """
443         propspec = propspec.items()
444         for propname, nodeid in propspec:
445 #            nodeid = str(nodeid)
446             # check the prop is OK
447             prop = self.properties[propname]
448             if not prop.isLinkType and not prop.isMultilinkType:
449                 raise TypeError, "'%s' not a Link/Multilink property"%propname
450             if not self.db.hasnode(prop.classname, nodeid):
451                 raise ValueError, '%s has no node %s'%(link_class, nodeid)
453         # ok, now do the find
454         cldb = self.db.getclassdb(self.classname)
455         l = []
456         for id in self.db.getnodeids(self.classname, cldb):
457             node = self.db.getnode(self.classname, id, cldb)
458             if node.has_key(self.db.RETIRED_FLAG):
459                 continue
460             for propname, nodeid in propspec:
461 #                nodeid = str(nodeid)
462                 property = node[propname]
463                 if prop.isLinkType and nodeid == property:
464                     l.append(id)
465                 elif prop.isMultilinkType and nodeid in property:
466                     l.append(id)
467         cldb.close()
468         return l
470     def stringFind(self, **requirements):
471         """Locate a particular node by matching a set of its String properties.
473         If the property is not a String property, a TypeError is raised.
474         
475         The return is a list of the id of all nodes that match.
476         """
477         for propname in requirements.keys():
478             prop = self.properties[propname]
479             if not prop.isStringType:
480                 raise TypeError, "'%s' not a String property"%propname
481         l = []
482         cldb = self.db.getclassdb(self.classname)
483         for nodeid in self.db.getnodeids(self.classname, cldb):
484             node = self.db.getnode(self.classname, nodeid, cldb)
485             if node.has_key(self.db.RETIRED_FLAG):
486                 continue
487             for key, value in requirements.items():
488                 if node[key] != value:
489                     break
490             else:
491                 l.append(nodeid)
492         cldb.close()
493         return l
495     def list(self):
496         """Return a list of the ids of the active nodes in this class."""
497         l = []
498         cn = self.classname
499         cldb = self.db.getclassdb(cn)
500         for nodeid in self.db.getnodeids(cn, cldb):
501             node = self.db.getnode(cn, nodeid, cldb)
502             if node.has_key(self.db.RETIRED_FLAG):
503                 continue
504             l.append(nodeid)
505         l.sort()
506         cldb.close()
507         return l
509     # XXX not in spec
510     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
511         ''' Return a list of the ids of the active nodes in this class that
512             match the 'filter' spec, sorted by the group spec and then the
513             sort spec
514         '''
515         cn = self.classname
517         # optimise filterspec
518         l = []
519         props = self.getprops()
520         for k, v in filterspec.items():
521             propclass = props[k]
522             if propclass.isLinkType:
523                 if type(v) is not type([]):
524                     v = [v]
525                 # replace key values with node ids
526                 u = []
527                 link_class =  self.db.classes[propclass.classname]
528                 for entry in v:
529                     if not num_re.match(entry):
530                         try:
531                             entry = link_class.lookup(entry)
532                         except:
533                             raise ValueError, 'new property "%s": %s not a %s'%(
534                                 k, entry, self.properties[k].classname)
535                     u.append(entry)
537                 l.append((0, k, u))
538             elif propclass.isMultilinkType:
539                 if type(v) is not type([]):
540                     v = [v]
541                 # replace key values with node ids
542                 u = []
543                 link_class =  self.db.classes[propclass.classname]
544                 for entry in v:
545                     if not num_re.match(entry):
546                         try:
547                             entry = link_class.lookup(entry)
548                         except:
549                             raise ValueError, 'new property "%s": %s not a %s'%(
550                                 k, entry, self.properties[k].classname)
551                     u.append(entry)
552                 l.append((1, k, u))
553             elif propclass.isStringType:
554                 v = v[0]
555                 if '*' in v or '?' in v:
556                     # simple glob searching
557                     v = v.replace('?', '.')
558                     v = v.replace('*', '.*?')
559                     v = re.compile(v)
560                     l.append((2, k, v))
561                 elif v[0] == '^':
562                     # start-anchored
563                     if v[-1] == '$':
564                         # _and_ end-anchored
565                         l.append((6, k, v[1:-1]))
566                     l.append((3, k, v[1:]))
567                 elif v[-1] == '$':
568                     # end-anchored
569                     l.append((4, k, v[:-1]))
570                 else:
571                     # substring
572                     l.append((5, k, v))
573             else:
574                 l.append((6, k, v))
575         filterspec = l
577         # now, find all the nodes that are active and pass filtering
578         l = []
579         cldb = self.db.getclassdb(cn)
580         for nodeid in self.db.getnodeids(cn, cldb):
581             node = self.db.getnode(cn, nodeid, cldb)
582             if node.has_key(self.db.RETIRED_FLAG):
583                 continue
584             # apply filter
585             for t, k, v in filterspec:
586                 if t == 0 and node[k] not in v:
587                     # link - if this node'd property doesn't appear in the
588                     # filterspec's nodeid list, skip it
589                     break
590                 elif t == 1:
591                     # multilink - if any of the nodeids required by the
592                     # filterspec aren't in this node's property, then skip
593                     # it
594                     for value in v:
595                         if value not in node[k]:
596                             break
597                     else:
598                         continue
599                     break
600                 elif t == 2 and not v.search(node[k]):
601                     # RE search
602                     break
603                 elif t == 3 and node[k][:len(v)] != v:
604                     # start anchored
605                     break
606                 elif t == 4 and node[k][-len(v):] != v:
607                     # end anchored
608                     break
609                 elif t == 5 and node[k].find(v) == -1:
610                     # substring search
611                     break
612                 elif t == 6 and node[k] != v:
613                     # straight value comparison for the other types
614                     break
615             else:
616                 l.append((nodeid, node))
617         l.sort()
618         cldb.close()
620         # optimise sort
621         m = []
622         for entry in sort:
623             if entry[0] != '-':
624                 m.append(('+', entry))
625             else:
626                 m.append((entry[0], entry[1:]))
627         sort = m
629         # optimise group
630         m = []
631         for entry in group:
632             if entry[0] != '-':
633                 m.append(('+', entry))
634             else:
635                 m.append((entry[0], entry[1:]))
636         group = m
638         # now, sort the result
639         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
640                 db = self.db, cl=self):
641             a_id, an = a
642             b_id, bn = b
643             # sort by group and then sort
644             for list in group, sort:
645                 for dir, prop in list:
646                     # handle the properties that might be "faked"
647                     if not an.has_key(prop):
648                         an[prop] = cl.get(a_id, prop)
649                     av = an[prop]
650                     if not bn.has_key(prop):
651                         bn[prop] = cl.get(b_id, prop)
652                     bv = bn[prop]
654                     # sorting is class-specific
655                     propclass = properties[prop]
657                     # String and Date values are sorted in the natural way
658                     if propclass.isStringType:
659                         # clean up the strings
660                         if av and av[0] in string.uppercase:
661                             av = an[prop] = av.lower()
662                         if bv and bv[0] in string.uppercase:
663                             bv = bn[prop] = bv.lower()
664                     if propclass.isStringType or propclass.isDateType:
665                         if dir == '+':
666                             r = cmp(av, bv)
667                             if r != 0: return r
668                         elif dir == '-':
669                             r = cmp(bv, av)
670                             if r != 0: return r
672                     # Link properties are sorted according to the value of
673                     # the "order" property on the linked nodes if it is
674                     # present; or otherwise on the key string of the linked
675                     # nodes; or finally on  the node ids.
676                     elif propclass.isLinkType:
677                         link = db.classes[propclass.classname]
678                         if link.getprops().has_key('order'):
679                             if dir == '+':
680                                 r = cmp(link.get(av, 'order'),
681                                     link.get(bv, 'order'))
682                                 if r != 0: return r
683                             elif dir == '-':
684                                 r = cmp(link.get(bv, 'order'),
685                                     link.get(av, 'order'))
686                                 if r != 0: return r
687                         elif link.getkey():
688                             key = link.getkey()
689                             if dir == '+':
690                                 r = cmp(link.get(av, key), link.get(bv, key))
691                                 if r != 0: return r
692                             elif dir == '-':
693                                 r = cmp(link.get(bv, key), link.get(av, key))
694                                 if r != 0: return r
695                         else:
696                             if dir == '+':
697                                 r = cmp(av, bv)
698                                 if r != 0: return r
699                             elif dir == '-':
700                                 r = cmp(bv, av)
701                                 if r != 0: return r
703                     # Multilink properties are sorted according to how many
704                     # links are present.
705                     elif propclass.isMultilinkType:
706                         if dir == '+':
707                             r = cmp(len(av), len(bv))
708                             if r != 0: return r
709                         elif dir == '-':
710                             r = cmp(len(bv), len(av))
711                             if r != 0: return r
712                 # end for dir, prop in list:
713             # end for list in sort, group:
714             # if all else fails, compare the ids
715             return cmp(a[0], b[0])
717         l.sort(sortfun)
718         return [i[0] for i in l]
720     def count(self):
721         """Get the number of nodes in this class.
723         If the returned integer is 'numnodes', the ids of all the nodes
724         in this class run from 1 to numnodes, and numnodes+1 will be the
725         id of the next node to be created in this class.
726         """
727         return self.db.countnodes(self.classname)
729     # Manipulating properties:
731     def getprops(self):
732         """Return a dictionary mapping property names to property objects."""
733         d = self.properties.copy()
734         d['id'] = String()
735         return d
737     def addprop(self, **properties):
738         """Add properties to this class.
740         The keyword arguments in 'properties' must map names to property
741         objects, or a TypeError is raised.  None of the keys in 'properties'
742         may collide with the names of existing properties, or a ValueError
743         is raised before any properties have been added.
744         """
745         for key in properties.keys():
746             if self.properties.has_key(key):
747                 raise ValueError, key
748         self.properties.update(properties)
751 # XXX not in spec
752 class Node:
753     ''' A convenience wrapper for the given node
754     '''
755     def __init__(self, cl, nodeid):
756         self.__dict__['cl'] = cl
757         self.__dict__['nodeid'] = nodeid
758     def keys(self):
759         return self.cl.getprops().keys()
760     def has_key(self, name):
761         return self.cl.getprops().has_key(name)
762     def __getattr__(self, name):
763         if self.__dict__.has_key(name):
764             return self.__dict__['name']
765         try:
766             return self.cl.get(self.nodeid, name)
767         except KeyError, value:
768             raise AttributeError, str(value)
769     def __getitem__(self, name):
770         return self.cl.get(self.nodeid, name)
771     def __setattr__(self, name, value):
772         try:
773             return self.cl.set(self.nodeid, **{name: value})
774         except KeyError, value:
775             raise AttributeError, str(value)
776     def __setitem__(self, name, value):
777         self.cl.set(self.nodeid, **{name: value})
778     def history(self):
779         return self.cl.history(self.nodeid)
780     def retire(self):
781         return self.cl.retire(self.nodeid)
784 def Choice(name, *options):
785     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
786     for i in range(len(options)):
787         cl.create(name=option[i], order=i)
788     return hyperdb.Link(name)
791 # $Log: not supported by cvs2svn $
792 # Revision 1.6  2001/07/29 05:36:14  richard
793 # Cleanup of the link label generation.
795 # Revision 1.5  2001/07/29 04:05:37  richard
796 # Added the fabricated property "id".
798 # Revision 1.4  2001/07/27 06:25:35  richard
799 # Fixed some of the exceptions so they're the right type.
800 # Removed the str()-ification of node ids so we don't mask oopsy errors any
801 # more.
803 # Revision 1.3  2001/07/27 05:17:14  richard
804 # just some comments
806 # Revision 1.2  2001/07/22 12:09:32  richard
807 # Final commit of Grande Splite
809 # Revision 1.1  2001/07/22 11:58:35  richard
810 # More Grande Splite
813 # vim: set filetype=python ts=4 sw=4 et si