Mercurial > p > roundup > code
comparison roundup/hyperdb.py @ 3682:193f316dbbe9
More transitive-property support.
- Implemented transitive properties in sort and group specs. Sort/group
specs can now be lists of specs.
- All regression tests except for one metakit backend test related to
metakit having no representation of NULL pass
- Fixed more PEP 8 whitespace peeves (and probably introduced some new
ones :-)
- Moved Proptree from support.py to hyperdb.py due to circular import
- Moved some proptree-specific methods from Class to Proptree
- Added a test for sorting by ids -> should be numeric sort (which now
really works for all backends)
- Added "required" attribute to all property classes in hyperdb (e.g.,
String, Link,...), see Feature Requests [SF#539081]
-> factored common stuff to _Type. Note that I also converted to a
new-style class when I was at it. Bad: The repr changes for new-style
classes which made some SQL backends break (!) because the repr of
Multilink is used in the schema storage. Fixed the repr to be
independent of the class type.
- Added get_required_props to Class. Todo: should also automagically
make the key property required...
- Add a sort_repr method to property classes. This defines the
sort-order. Individual backends may use diffent routines if the
outcome is the same. This one has a special case for id properties to
make the sorting numeric. Using these methods isn't mandatory in
backends as long as the sort-order is correct.
- Multilink sorting takes orderprop into account. It used to sort by
ids. You can restore the old behaviour by specifying id as the
orderprop of the Multilink if you really need that.
- If somebody specified a Link or Multilink as orderprop, we sort by
labelprop of that class -- not transitively by orderprop. I've
resited the tempation to implement recursive orderprop here: There
could even be loops if several classes specify a Link or Multilink as
the orderprop...
- Fixed a bug in Metakit-Backend: When sorting by Links, the backend
would do a natural join to the Link class. It would rename the "id"
attribute before joining but *not* all the other attributes of the
joined class. So in one test-case we had a name-clash with
priority.name and status.name when sorting *and* grouping by these
attributes. Depending on the order of joining this would produce a
name-clash with broken sort-results (and broken display if the
original class has an attribute that clashes). I'm now doing the
sorting of Links in the generic filter method for the metakit backend.
I've left the dead code in the metakit-backend since correctly
implementing this in the backend will probably be more efficient.
- updated doc/design.html with the new docstring of filter.
| author | Ralf Schlatterbeck <schlatterbeck@users.sourceforge.net> |
|---|---|
| date | Mon, 21 Aug 2006 12:19:48 +0000 |
| parents | a15c15510e99 |
| children | bffa231ec3bc |
comparison
equal
deleted
inserted
replaced
| 3681:b9301ae1c34d | 3682:193f316dbbe9 |
|---|---|
| 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
| 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" | 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" |
| 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, | 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, |
| 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. | 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. |
| 17 # | 17 # |
| 18 # $Id: hyperdb.py,v 1.123 2006-08-11 04:50:24 richard Exp $ | 18 # $Id: hyperdb.py,v 1.124 2006-08-21 12:19:48 schlatterbeck Exp $ |
| 19 | 19 |
| 20 """Hyperdatabase implementation, especially field types. | 20 """Hyperdatabase implementation, especially field types. |
| 21 """ | 21 """ |
| 22 __docformat__ = 'restructuredtext' | 22 __docformat__ = 'restructuredtext' |
| 23 | 23 |
| 24 # standard python modules | 24 # standard python modules |
| 25 import sys, os, time, re, shutil, weakref | 25 import sys, os, time, re, shutil, weakref |
| 26 from sets import Set | |
| 26 | 27 |
| 27 # roundup modules | 28 # roundup modules |
| 28 import date, password | 29 import date, password |
| 29 from support import ensureParentsExist, PrioList, Proptree | 30 from support import ensureParentsExist, PrioList, sorted, reversed |
| 30 | 31 |
| 31 # | 32 # |
| 32 # Types | 33 # Types |
| 33 # | 34 # |
| 34 class String: | 35 class _Type(object): |
| 35 """An object designating a String property.""" | 36 """A roundup property type.""" |
| 36 def __init__(self, indexme='no'): | 37 def __init__(self, required=False): |
| 37 self.indexme = indexme == 'yes' | 38 self.required = required |
| 38 def __repr__(self): | 39 def __repr__(self): |
| 39 ' more useful for dumps ' | 40 ' more useful for dumps ' |
| 40 return '<%s>'%self.__class__ | 41 return '<%s.%s>'%(self.__class__.__module__, self.__class__.__name__) |
| 42 def sort_repr (self, cls, val, name): | |
| 43 """Representation used for sorting. This should be a python | |
| 44 built-in type, otherwise sorting will take ages. Note that | |
| 45 individual backends may chose to use something different for | |
| 46 sorting as long as the outcome is the same. | |
| 47 """ | |
| 48 return val | |
| 49 | |
| 50 class String(_Type): | |
| 51 """An object designating a String property.""" | |
| 52 def __init__(self, indexme='no', required=False): | |
| 53 super(String, self).__init__(required) | |
| 54 self.indexme = indexme == 'yes' | |
| 41 def from_raw(self, value, **kw): | 55 def from_raw(self, value, **kw): |
| 42 """fix the CRLF/CR -> LF stuff""" | 56 """fix the CRLF/CR -> LF stuff""" |
| 43 return fixNewlines(value) | 57 return fixNewlines(value) |
| 44 | 58 def sort_repr (self, cls, val, name): |
| 45 class Password: | 59 if name == 'id': |
| 60 return int(val) | |
| 61 return val | |
| 62 | |
| 63 class Password(_Type): | |
| 46 """An object designating a Password property.""" | 64 """An object designating a Password property.""" |
| 47 def __repr__(self): | |
| 48 ' more useful for dumps ' | |
| 49 return '<%s>'%self.__class__ | |
| 50 def from_raw(self, value, **kw): | 65 def from_raw(self, value, **kw): |
| 51 if not value: | 66 if not value: |
| 52 return None | 67 return None |
| 53 m = password.Password.pwre.match(value) | 68 m = password.Password.pwre.match(value) |
| 54 if m: | 69 if m: |
| 64 try: | 79 try: |
| 65 value = password.Password(value) | 80 value = password.Password(value) |
| 66 except password.PasswordValueError, message: | 81 except password.PasswordValueError, message: |
| 67 raise HyperdbValueError, 'property %s: %s'%(propname, message) | 82 raise HyperdbValueError, 'property %s: %s'%(propname, message) |
| 68 return value | 83 return value |
| 69 | 84 def sort_repr (self, cls, val, name): |
| 70 class Date: | 85 if not val: |
| 86 return val | |
| 87 return str(val) | |
| 88 | |
| 89 class Date(_Type): | |
| 71 """An object designating a Date property.""" | 90 """An object designating a Date property.""" |
| 72 def __init__(self, offset = None): | 91 def __init__(self, offset=None, required=False): |
| 92 super(Date, self).__init__(required) | |
| 73 self._offset = offset | 93 self._offset = offset |
| 74 def __repr__(self): | 94 def offset(self, db): |
| 75 ' more useful for dumps ' | 95 if self._offset is not None: |
| 76 return '<%s>'%self.__class__ | |
| 77 def offset (self, db) : | |
| 78 if self._offset is not None : | |
| 79 return self._offset | 96 return self._offset |
| 80 return db.getUserTimezone () | 97 return db.getUserTimezone() |
| 81 def from_raw(self, value, db, **kw): | 98 def from_raw(self, value, db, **kw): |
| 82 try: | 99 try: |
| 83 value = date.Date(value, self.offset(db)) | 100 value = date.Date(value, self.offset(db)) |
| 84 except ValueError, message: | 101 except ValueError, message: |
| 85 raise HyperdbValueError, 'property %s: %r is an invalid '\ | 102 raise HyperdbValueError, 'property %s: %r is an invalid '\ |
| 86 'date (%s)'%(kw['propname'], value, message) | 103 'date (%s)'%(kw['propname'], value, message) |
| 87 return value | 104 return value |
| 88 def range_from_raw(self, value, db): | 105 def range_from_raw(self, value, db): |
| 89 """return Range value from given raw value with offset correction""" | 106 """return Range value from given raw value with offset correction""" |
| 90 return date.Range(value, date.Date, offset=self.offset(db)) | 107 return date.Range(value, date.Date, offset=self.offset(db)) |
| 91 | 108 def sort_repr (self, cls, val, name): |
| 92 class Interval: | 109 if not val: |
| 110 return val | |
| 111 return str(val) | |
| 112 | |
| 113 class Interval(_Type): | |
| 93 """An object designating an Interval property.""" | 114 """An object designating an Interval property.""" |
| 94 def __repr__(self): | |
| 95 ' more useful for dumps ' | |
| 96 return '<%s>'%self.__class__ | |
| 97 def from_raw(self, value, **kw): | 115 def from_raw(self, value, **kw): |
| 98 try: | 116 try: |
| 99 value = date.Interval(value) | 117 value = date.Interval(value) |
| 100 except ValueError, message: | 118 except ValueError, message: |
| 101 raise HyperdbValueError, 'property %s: %r is an invalid '\ | 119 raise HyperdbValueError, 'property %s: %r is an invalid '\ |
| 102 'date interval (%s)'%(kw['propname'], value, message) | 120 'date interval (%s)'%(kw['propname'], value, message) |
| 103 return value | 121 return value |
| 104 | 122 def sort_repr (self, cls, val, name): |
| 105 class Link: | 123 if not val: |
| 106 """An object designating a Link property that links to a | 124 return val |
| 107 node in a specified class.""" | 125 return val.as_seconds() |
| 108 def __init__(self, classname, do_journal='yes'): | 126 |
| 109 ''' Default is to not journal link and unlink events | 127 class _Pointer(_Type): |
| 110 ''' | 128 """An object designating a Pointer property that links or multilinks |
| 129 to a node in a specified class.""" | |
| 130 def __init__(self, classname, do_journal='yes', required=False): | |
| 131 ''' Default is to journal link and unlink events | |
| 132 ''' | |
| 133 super(_Pointer, self).__init__(required) | |
| 111 self.classname = classname | 134 self.classname = classname |
| 112 self.do_journal = do_journal == 'yes' | 135 self.do_journal = do_journal == 'yes' |
| 113 def __repr__(self): | 136 def __repr__(self): |
| 114 ' more useful for dumps ' | 137 """more useful for dumps. But beware: This is also used in schema |
| 115 return '<%s to "%s">'%(self.__class__, self.classname) | 138 storage in SQL backends! |
| 139 """ | |
| 140 return '<%s.%s to "%s">'%(self.__class__.__module__, | |
| 141 self.__class__.__name__, self.classname) | |
| 142 | |
| 143 class Link(_Pointer): | |
| 144 """An object designating a Link property that links to a | |
| 145 node in a specified class.""" | |
| 116 def from_raw(self, value, db, propname, **kw): | 146 def from_raw(self, value, db, propname, **kw): |
| 117 if value == '-1' or not value: | 147 if value == '-1' or not value: |
| 118 value = None | 148 value = None |
| 119 else: | 149 else: |
| 120 value = convertLinkValue(db, propname, self, value) | 150 value = convertLinkValue(db, propname, self, value) |
| 121 return value | 151 return value |
| 122 | 152 def sort_repr (self, cls, val, name): |
| 123 class Multilink: | 153 if not val: |
| 154 return val | |
| 155 op = cls.labelprop() | |
| 156 if op == 'id': | |
| 157 return int(cls.get(val, op)) | |
| 158 return cls.get(val, op) | |
| 159 | |
| 160 class Multilink(_Pointer): | |
| 124 """An object designating a Multilink property that links | 161 """An object designating a Multilink property that links |
| 125 to nodes in a specified class. | 162 to nodes in a specified class. |
| 126 | 163 |
| 127 "classname" indicates the class to link to | 164 "classname" indicates the class to link to |
| 128 | 165 |
| 129 "do_journal" indicates whether the linked-to nodes should have | 166 "do_journal" indicates whether the linked-to nodes should have |
| 130 'link' and 'unlink' events placed in their journal | 167 'link' and 'unlink' events placed in their journal |
| 131 """ | 168 """ |
| 132 def __init__(self, classname, do_journal='yes'): | |
| 133 ''' Default is to not journal link and unlink events | |
| 134 ''' | |
| 135 self.classname = classname | |
| 136 self.do_journal = do_journal == 'yes' | |
| 137 def __repr__(self): | |
| 138 ' more useful for dumps ' | |
| 139 return '<%s to "%s">'%(self.__class__, self.classname) | |
| 140 def from_raw(self, value, db, klass, propname, itemid, **kw): | 169 def from_raw(self, value, db, klass, propname, itemid, **kw): |
| 141 if not value: | 170 if not value: |
| 142 return [] | 171 return [] |
| 143 | 172 |
| 144 # get the current item value if it's not a new item | 173 # get the current item value if it's not a new item |
| 201 value = [int(x) for x in value] | 230 value = [int(x) for x in value] |
| 202 value.sort() | 231 value.sort() |
| 203 value = [str(x) for x in value] | 232 value = [str(x) for x in value] |
| 204 return value | 233 return value |
| 205 | 234 |
| 206 class Boolean: | 235 def sort_repr (self, cls, val, name): |
| 236 if not val: | |
| 237 return val | |
| 238 op = cls.labelprop() | |
| 239 if op == 'id': | |
| 240 return [int(cls.get(v, op)) for v in val] | |
| 241 return [cls.get(v, op) for v in val] | |
| 242 | |
| 243 class Boolean(_Type): | |
| 207 """An object designating a boolean property""" | 244 """An object designating a boolean property""" |
| 208 def __repr__(self): | |
| 209 'more useful for dumps' | |
| 210 return '<%s>' % self.__class__ | |
| 211 def from_raw(self, value, **kw): | 245 def from_raw(self, value, **kw): |
| 212 value = value.strip() | 246 value = value.strip() |
| 213 # checked is a common HTML checkbox value | 247 # checked is a common HTML checkbox value |
| 214 value = value.lower() in ('checked', 'yes', 'true', 'on', '1') | 248 value = value.lower() in ('checked', 'yes', 'true', 'on', '1') |
| 215 return value | 249 return value |
| 216 | 250 |
| 217 class Number: | 251 class Number(_Type): |
| 218 """An object designating a numeric property""" | 252 """An object designating a numeric property""" |
| 219 def __repr__(self): | |
| 220 'more useful for dumps' | |
| 221 return '<%s>' % self.__class__ | |
| 222 def from_raw(self, value, **kw): | 253 def from_raw(self, value, **kw): |
| 223 value = value.strip() | 254 value = value.strip() |
| 224 try: | 255 try: |
| 225 value = float(value) | 256 value = float(value) |
| 226 except ValueError: | 257 except ValueError: |
| 238 m = dre.match(designator) | 269 m = dre.match(designator) |
| 239 if m is None: | 270 if m is None: |
| 240 raise DesignatorError, '"%s" not a node designator'%designator | 271 raise DesignatorError, '"%s" not a node designator'%designator |
| 241 return m.group(1), m.group(2) | 272 return m.group(1), m.group(2) |
| 242 | 273 |
| 274 class Proptree(object): | |
| 275 ''' Simple tree data structure for optimizing searching of | |
| 276 properties. Each node in the tree represents a roundup Class | |
| 277 Property that has to be navigated for finding the given search | |
| 278 or sort properties. The sort_type attribute is used for | |
| 279 distinguishing nodes in the tree used for sorting or searching: If | |
| 280 it is 0 for a node, that node is not used for sorting. If it is 1, | |
| 281 it is used for both, sorting and searching. If it is 2 it is used | |
| 282 for sorting only. | |
| 283 | |
| 284 The Proptree is also used for transitively searching attributes for | |
| 285 backends that do not support transitive search (e.g. anydbm). The | |
| 286 _val attribute with set_val is used for this. | |
| 287 ''' | |
| 288 | |
| 289 def __init__(self, db, cls, name, props, parent = None): | |
| 290 self.db = db | |
| 291 self.name = name | |
| 292 self.props = props | |
| 293 self.parent = parent | |
| 294 self._val = None | |
| 295 self.has_values = False | |
| 296 self.cls = cls | |
| 297 self.classname = None | |
| 298 self.uniqname = None | |
| 299 self.children = [] | |
| 300 self.sortattr = [] | |
| 301 self.propdict = {} | |
| 302 self.sort_type = 0 | |
| 303 self.sort_direction = None | |
| 304 self.sort_ids = None | |
| 305 self.sort_ids_needed = False | |
| 306 self.sort_result = None | |
| 307 self.attr_sort_done = False | |
| 308 self.tree_sort_done = False | |
| 309 self.propclass = None | |
| 310 if parent: | |
| 311 self.root = parent.root | |
| 312 self.depth = parent.depth + 1 | |
| 313 else: | |
| 314 self.root = self | |
| 315 self.seqno = 1 | |
| 316 self.depth = 0 | |
| 317 self.sort_type = 1 | |
| 318 self.id = self.root.seqno | |
| 319 self.root.seqno += 1 | |
| 320 if self.cls: | |
| 321 self.classname = self.cls.classname | |
| 322 self.uniqname = '%s%s' % (self.cls.classname, self.id) | |
| 323 if not self.parent: | |
| 324 self.uniqname = self.cls.classname | |
| 325 | |
| 326 def append(self, name, sort_type = 0): | |
| 327 """Append a property to self.children. Will create a new | |
| 328 propclass for the child. | |
| 329 """ | |
| 330 if name in self.propdict: | |
| 331 pt = self.propdict[name] | |
| 332 if sort_type and not pt.sort_type: | |
| 333 pt.sort_type = 1 | |
| 334 return pt | |
| 335 propclass = self.props[name] | |
| 336 cls = None | |
| 337 props = None | |
| 338 if isinstance(propclass, (Link, Multilink)): | |
| 339 cls = self.db.getclass(propclass.classname) | |
| 340 props = cls.getprops() | |
| 341 child = self.__class__(self.db, cls, name, props, parent = self) | |
| 342 child.sort_type = sort_type | |
| 343 child.propclass = propclass | |
| 344 self.children.append(child) | |
| 345 self.propdict[name] = child | |
| 346 return child | |
| 347 | |
| 348 def compute_sort_done(self, mlseen=False): | |
| 349 """ Recursively check if attribute is needed for sorting | |
| 350 (self.sort_type > 0) or all children have tree_sort_done set and | |
| 351 sort_ids_needed unset: set self.tree_sort_done if one of the conditions | |
| 352 holds. Also remove sort_ids_needed recursively once having seen a | |
| 353 Multilink. | |
| 354 """ | |
| 355 if isinstance (self.propclass, Multilink): | |
| 356 mlseen = True | |
| 357 if mlseen: | |
| 358 self.sort_ids_needed = False | |
| 359 self.tree_sort_done = True | |
| 360 for p in self.children: | |
| 361 p.compute_sort_done(mlseen) | |
| 362 if not p.tree_sort_done: | |
| 363 self.tree_sort_done = False | |
| 364 if not self.sort_type: | |
| 365 self.tree_sort_done = True | |
| 366 if mlseen: | |
| 367 self.tree_sort_done = False | |
| 368 | |
| 369 def ancestors(self): | |
| 370 p = self | |
| 371 while p.parent: | |
| 372 yield p | |
| 373 p = p.parent | |
| 374 | |
| 375 def search(self, search_matches=None, sort=True): | |
| 376 """ Recursively search for the given properties in a proptree. | |
| 377 Once all properties are non-transitive, the search generates a | |
| 378 simple _filter call which does the real work | |
| 379 """ | |
| 380 filterspec = {} | |
| 381 for p in self.children: | |
| 382 if p.sort_type < 2: | |
| 383 if p.children: | |
| 384 p.search(sort = False) | |
| 385 filterspec[p.name] = p.val | |
| 386 self.val = self.cls._filter(search_matches, filterspec, sort and self) | |
| 387 return self.val | |
| 388 | |
| 389 def sort (self, ids=None): | |
| 390 """ Sort ids by the order information stored in self. With | |
| 391 optimisations: Some order attributes may be precomputed (by the | |
| 392 backend) and some properties may already be sorted. | |
| 393 """ | |
| 394 if ids is None: | |
| 395 ids = self.val | |
| 396 if self.sortattr and [s for s in self.sortattr if not s.attr_sort_done]: | |
| 397 return self._searchsort(ids, True, True) | |
| 398 return ids | |
| 399 | |
| 400 def sortable_children(self, intermediate=False): | |
| 401 """ All children needed for sorting. If intermediate is True, | |
| 402 intermediate nodes (not being a sort attribute) are returned, | |
| 403 too. | |
| 404 """ | |
| 405 return [p for p in self.children | |
| 406 if p.sort_type > 0 and (intermediate or p.sort_direction)] | |
| 407 | |
| 408 def __iter__(self): | |
| 409 """ Yield nodes in depth-first order -- visited nodes first """ | |
| 410 for p in self.children: | |
| 411 yield p | |
| 412 for c in p: | |
| 413 yield c | |
| 414 | |
| 415 def _get (self, ids): | |
| 416 """Lookup given ids -- possibly a list of list. We recurse until | |
| 417 we have a list of ids. | |
| 418 """ | |
| 419 if not ids: | |
| 420 return ids | |
| 421 if isinstance (ids[0], list): | |
| 422 cids = [self._get(i) for i in ids] | |
| 423 else: | |
| 424 cids = [i and self.parent.cls.get(i, self.name) for i in ids] | |
| 425 if self.sortattr: | |
| 426 cids = [self._searchsort(i, False, True) for i in cids] | |
| 427 return cids | |
| 428 | |
| 429 def _searchsort(self, ids=None, update=True, dosort=True): | |
| 430 """ Recursively compute the sort attributes. Note that ids | |
| 431 may be a deeply nested list of lists of ids if several | |
| 432 multilinks are encountered on the way from the root to an | |
| 433 individual attribute. We make sure that everything is properly | |
| 434 sorted on the way up. Note that the individual backend may | |
| 435 already have precomputed self.result or self.sort_ids. In this | |
| 436 case we do nothing for existing sa.result and recurse further if | |
| 437 self.sort_ids is available. | |
| 438 | |
| 439 Yech, Multilinks: This gets especially complicated if somebody | |
| 440 sorts by different attributes of the same multilink (or | |
| 441 transitively across several multilinks). My use-case is sorting | |
| 442 by issue.messages.author and (reverse) by issue.messages.date. | |
| 443 In this case we sort the messages by author and date and use | |
| 444 this sorted list twice for sorting issues. This means that | |
| 445 issues are sorted by author and then by the time of the messages | |
| 446 *of this author*. Probably what the user intends in that case, | |
| 447 so we do *not* use two sorted lists of messages, one sorted by | |
| 448 author and one sorted by date for sorting issues. | |
| 449 """ | |
| 450 for pt in self.sortable_children(intermediate = True): | |
| 451 # ids can be an empty list | |
| 452 if pt.tree_sort_done or not ids: | |
| 453 continue | |
| 454 if pt.sort_ids: # cached or computed by backend | |
| 455 cids = pt.sort_ids | |
| 456 else: | |
| 457 cids = pt._get(ids) | |
| 458 if pt.sort_direction and not pt.sort_result: | |
| 459 sortrep = pt.propclass.sort_repr | |
| 460 pt.sort_result = pt._sort_repr(sortrep, cids) | |
| 461 pt.sort_ids = cids | |
| 462 if pt.children: | |
| 463 pt._searchsort(cids, update, False) | |
| 464 if self.sortattr and dosort: | |
| 465 ids = self._sort(ids) | |
| 466 if not update: | |
| 467 for pt in self.sortable_children(intermediate = True): | |
| 468 pt.sort_ids = None | |
| 469 for pt in self.sortattr: | |
| 470 pt.sort_result = None | |
| 471 return ids | |
| 472 | |
| 473 def _set_val(self, val): | |
| 474 """Check if self._val is already defined. If yes, we compute the | |
| 475 intersection of the old and the new value(s) | |
| 476 """ | |
| 477 if self.has_values: | |
| 478 v = self._val | |
| 479 if not isinstance(self._val, type([])): | |
| 480 v = [self._val] | |
| 481 vals = Set(v) | |
| 482 vals.intersection_update(val) | |
| 483 self._val = [v for v in vals] | |
| 484 else: | |
| 485 self._val = val | |
| 486 self.has_values = True | |
| 487 | |
| 488 val = property(lambda self: self._val, _set_val) | |
| 489 | |
| 490 def _sort(self, val): | |
| 491 """Finally sort by the given sortattr.sort_result. Note that we | |
| 492 do not sort by attrs having attr_sort_done set. The caller is | |
| 493 responsible for setting attr_sort_done only for trailing | |
| 494 attributes (otherwise the sort order is wrong). Since pythons | |
| 495 sort is stable, we can sort already sorted lists without | |
| 496 destroying the sort-order for items that compare equal with the | |
| 497 current sort. | |
| 498 | |
| 499 Sorting-Strategy: We sort repeatedly by different sort-keys from | |
| 500 right to left. Since pythons sort is stable, we can safely do | |
| 501 that. An optimisation is a "run-length encoding" of the | |
| 502 sort-directions: If several sort attributes sort in the same | |
| 503 direction we can combine them into a single sort. Note that | |
| 504 repeated sorting is probably more efficient than using | |
| 505 compare-methods in python due to the overhead added by compare | |
| 506 methods. | |
| 507 """ | |
| 508 if not val: | |
| 509 return val | |
| 510 sortattr = [] | |
| 511 directions = [] | |
| 512 dir_idx = [] | |
| 513 idx = 0 | |
| 514 curdir = None | |
| 515 for sa in self.sortattr: | |
| 516 if sa.attr_sort_done: | |
| 517 break | |
| 518 if sortattr: | |
| 519 assert len(sortattr[0]) == len(sa.sort_result) | |
| 520 sortattr.append (sa.sort_result) | |
| 521 if curdir != sa.sort_direction: | |
| 522 dir_idx.append (idx) | |
| 523 directions.append (sa.sort_direction) | |
| 524 curdir = sa.sort_direction | |
| 525 idx += 1 | |
| 526 sortattr.append (val) | |
| 527 #print >> sys.stderr, "\nsortattr", sortattr | |
| 528 sortattr = zip (*sortattr) | |
| 529 for dir, i in reversed(zip(directions, dir_idx)): | |
| 530 rev = dir == '-' | |
| 531 sortattr = sorted (sortattr, key = lambda x:x[i:idx], reverse = rev) | |
| 532 idx = i | |
| 533 return [x[-1] for x in sortattr] | |
| 534 | |
| 535 def _sort_repr(self, sortrep, ids): | |
| 536 """Call sortrep for given ids -- possibly a list of list. We | |
| 537 recurse until we have a list of ids. | |
| 538 """ | |
| 539 if not ids: | |
| 540 return ids | |
| 541 if isinstance (ids[0], list): | |
| 542 res = [self._sort_repr(sortrep, i) for i in ids] | |
| 543 else: | |
| 544 res = [sortrep(self.cls, i, self.name) for i in ids] | |
| 545 return res | |
| 546 | |
| 547 def __repr__(self): | |
| 548 r = ["proptree:" + self.name] | |
| 549 for n in self: | |
| 550 r.append("proptree:" + " " * n.depth + n.name) | |
| 551 return '\n'.join(r) | |
| 552 __str__ = __repr__ | |
| 553 | |
| 243 # | 554 # |
| 244 # the base Database class | 555 # the base Database class |
| 245 # | 556 # |
| 246 class DatabaseError(ValueError): | 557 class DatabaseError(ValueError): |
| 247 '''Error to be raised when there is some problem in the database code | 558 '''Error to be raised when there is some problem in the database code |
| 464 self.do_journal = 1 | 775 self.do_journal = 1 |
| 465 | 776 |
| 466 # do the db-related init stuff | 777 # do the db-related init stuff |
| 467 db.addclass(self) | 778 db.addclass(self) |
| 468 | 779 |
| 469 actions = "create set retire restore".split () | 780 actions = "create set retire restore".split() |
| 470 self.auditors = dict ([(a, PrioList ()) for a in actions]) | 781 self.auditors = dict([(a, PrioList()) for a in actions]) |
| 471 self.reactors = dict ([(a, PrioList ()) for a in actions]) | 782 self.reactors = dict([(a, PrioList()) for a in actions]) |
| 472 | 783 |
| 473 def __repr__(self): | 784 def __repr__(self): |
| 474 '''Slightly more useful representation | 785 '''Slightly more useful representation |
| 475 ''' | 786 ''' |
| 476 return '<hyperdb.Class "%s">'%self.classname | 787 return '<hyperdb.Class "%s">'%self.classname |
| 616 None, or a TypeError is raised. The values of the key property on | 927 None, or a TypeError is raised. The values of the key property on |
| 617 all existing nodes must be unique or a ValueError is raised. | 928 all existing nodes must be unique or a ValueError is raised. |
| 618 """ | 929 """ |
| 619 raise NotImplementedError | 930 raise NotImplementedError |
| 620 | 931 |
| 621 def setlabelprop (self, labelprop): | 932 def setlabelprop(self, labelprop): |
| 622 """Set the label property. Used for override of labelprop | 933 """Set the label property. Used for override of labelprop |
| 623 resolution order. | 934 resolution order. |
| 624 """ | 935 """ |
| 625 if labelprop not in self.getprops () : | 936 if labelprop not in self.getprops(): |
| 626 raise ValueError, "Not a property name: %s" % labelprop | 937 raise ValueError, "Not a property name: %s" % labelprop |
| 627 self._labelprop = labelprop | 938 self._labelprop = labelprop |
| 628 | 939 |
| 629 def setorderprop (self, orderprop): | 940 def setorderprop(self, orderprop): |
| 630 """Set the order property. Used for override of orderprop | 941 """Set the order property. Used for override of orderprop |
| 631 resolution order | 942 resolution order |
| 632 """ | 943 """ |
| 633 if orderprop not in self.getprops () : | 944 if orderprop not in self.getprops(): |
| 634 raise ValueError, "Not a property name: %s" % orderprop | 945 raise ValueError, "Not a property name: %s" % orderprop |
| 635 self._orderprop = orderprop | 946 self._orderprop = orderprop |
| 636 | 947 |
| 637 def getkey(self): | 948 def getkey(self): |
| 638 """Return the name of the key property for this class or None.""" | 949 """Return the name of the key property for this class or None.""" |
| 648 1. key property | 959 1. key property |
| 649 2. "name" property | 960 2. "name" property |
| 650 3. "title" property | 961 3. "title" property |
| 651 4. first property from the sorted property name list | 962 4. first property from the sorted property name list |
| 652 """ | 963 """ |
| 653 if hasattr (self, '_labelprop') : | 964 if hasattr(self, '_labelprop'): |
| 654 return self._labelprop | 965 return self._labelprop |
| 655 k = self.getkey() | 966 k = self.getkey() |
| 656 if k: | 967 if k: |
| 657 return k | 968 return k |
| 658 props = self.getprops() | 969 props = self.getprops() |
| 664 return 'id' | 975 return 'id' |
| 665 props = props.keys() | 976 props = props.keys() |
| 666 props.sort() | 977 props.sort() |
| 667 return props[0] | 978 return props[0] |
| 668 | 979 |
| 669 def orderprop (self): | 980 def orderprop(self): |
| 670 """Return the property name to use for sorting for the given node. | 981 """Return the property name to use for sorting for the given node. |
| 671 | 982 |
| 672 This method computes the property for sorting. | 983 This method computes the property for sorting. |
| 673 It tries the following in order: | 984 It tries the following in order: |
| 674 | 985 |
| 675 0. self._orderprop if set | 986 0. self._orderprop if set |
| 676 1. "order" property | 987 1. "order" property |
| 677 2. self.labelprop () | 988 2. self.labelprop() |
| 678 """ | 989 """ |
| 679 | 990 |
| 680 if hasattr (self, '_orderprop') : | 991 if hasattr(self, '_orderprop'): |
| 681 return self._orderprop | 992 return self._orderprop |
| 682 props = self.getprops () | 993 props = self.getprops() |
| 683 if props.has_key ('order'): | 994 if props.has_key('order'): |
| 684 return 'order' | 995 return 'order' |
| 685 return self.labelprop () | 996 return self.labelprop() |
| 686 | 997 |
| 687 def lookup(self, keyvalue): | 998 def lookup(self, keyvalue): |
| 688 """Locate a particular node by its key property and return its id. | 999 """Locate a particular node by its key property and return its id. |
| 689 | 1000 |
| 690 If this class has no key property, a TypeError is raised. If the | 1001 If this class has no key property, a TypeError is raised. If the |
| 716 """For some backends this implements the non-transitive | 1027 """For some backends this implements the non-transitive |
| 717 search, for more information see the filter method. | 1028 search, for more information see the filter method. |
| 718 """ | 1029 """ |
| 719 raise NotImplementedError | 1030 raise NotImplementedError |
| 720 | 1031 |
| 721 def _proptree (self, filterspec): | 1032 def _proptree(self, filterspec, sortattr=[]): |
| 722 """Build a tree of all transitive properties in the given | 1033 """Build a tree of all transitive properties in the given |
| 723 filterspec. | 1034 filterspec. |
| 724 """ | 1035 """ |
| 725 proptree = Proptree (self.db, self, '', self.getprops ()) | 1036 proptree = Proptree(self.db, self, '', self.getprops()) |
| 726 for key, v in filterspec.iteritems (): | 1037 for key, v in filterspec.iteritems(): |
| 727 keys = key.split ('.') | 1038 keys = key.split('.') |
| 728 p = proptree | 1039 p = proptree |
| 729 for k in keys: | 1040 for k in keys: |
| 730 p = p.append (k) | 1041 p = p.append(k) |
| 731 p.val = v | 1042 p.val = v |
| 1043 multilinks = {} | |
| 1044 for s in sortattr: | |
| 1045 keys = s[1].split('.') | |
| 1046 p = proptree | |
| 1047 for k in keys: | |
| 1048 p = p.append(k, sort_type = 2) | |
| 1049 if isinstance (p.propclass, Multilink): | |
| 1050 multilinks[p] = True | |
| 1051 if p.cls: | |
| 1052 p = p.append(p.cls.orderprop(), sort_type = 2) | |
| 1053 if p.sort_direction: # if an orderprop is also specified explicitly | |
| 1054 continue | |
| 1055 p.sort_direction = s[0] | |
| 1056 proptree.sortattr.append (p) | |
| 1057 for p in multilinks.iterkeys(): | |
| 1058 sattr = {} | |
| 1059 for c in p: | |
| 1060 if c.sort_direction: | |
| 1061 sattr [c] = True | |
| 1062 for sa in proptree.sortattr: | |
| 1063 if sa in sattr: | |
| 1064 p.sortattr.append (sa) | |
| 732 return proptree | 1065 return proptree |
| 733 | 1066 |
| 734 def _propsearch (self, search_matches, proptree, sort, group): | 1067 def get_transitive_prop(self, propname_path, default = None): |
| 735 """ Recursively search for the given properties in proptree. | |
| 736 Once all properties are non-transitive, the search generates a | |
| 737 simple _filter call which does the real work | |
| 738 """ | |
| 739 for p in proptree.children: | |
| 740 if not p.children: | |
| 741 continue | |
| 742 p.val = p.cls._propsearch (None, p, (None, None), (None, None)) | |
| 743 filterspec = dict ([(p.name, p.val) for p in proptree.children]) | |
| 744 return self._filter (search_matches, filterspec, sort, group) | |
| 745 | |
| 746 def get_transitive_prop (self, propname_path, default = None) : | |
| 747 """Expand a transitive property (individual property names | 1068 """Expand a transitive property (individual property names |
| 748 separated by '.' into a new property at the end of the path. If | 1069 separated by '.' into a new property at the end of the path. If |
| 749 one of the names does not refer to a valid property, we return | 1070 one of the names does not refer to a valid property, we return |
| 750 None. | 1071 None. |
| 751 Example propname_path (for class issue): "messages.author" | 1072 Example propname_path (for class issue): "messages.author" |
| 752 """ | 1073 """ |
| 753 props = self.db.getclass(self.classname).getprops() | 1074 props = self.db.getclass(self.classname).getprops() |
| 754 for k in propname_path.split('.'): | 1075 for k in propname_path.split('.'): |
| 755 try : | 1076 try: |
| 756 prop = props[k] | 1077 prop = props[k] |
| 757 except KeyError, TypeError: | 1078 except KeyError, TypeError: |
| 758 return default | 1079 return default |
| 759 cl = getattr (prop, 'classname', None) | 1080 cl = getattr(prop, 'classname', None) |
| 760 props = None | 1081 props = None |
| 761 if cl: | 1082 if cl: |
| 762 props = self.db.getclass (cl).getprops() | 1083 props = self.db.getclass(cl).getprops() |
| 763 return prop | 1084 return prop |
| 764 | 1085 |
| 765 def filter(self, search_matches, filterspec, sort=(None,None), | 1086 def _sortattr(self, sort=[], group=[]): |
| 766 group=(None,None)): | 1087 """Build a single list of sort attributes in the correct order |
| 1088 with sanity checks (no duplicate properties) included. Always | |
| 1089 sort last by id -- if id is not already in sortattr. | |
| 1090 """ | |
| 1091 seen = {} | |
| 1092 sortattr = [] | |
| 1093 for srt in group, sort: | |
| 1094 if not isinstance(srt, list): | |
| 1095 srt = [srt] | |
| 1096 for s in srt: | |
| 1097 if s[1] and s[1] not in seen: | |
| 1098 sortattr.append((s[0] or '+', s[1])) | |
| 1099 seen[s[1]] = True | |
| 1100 if 'id' not in seen : | |
| 1101 sortattr.append(('+', 'id')) | |
| 1102 return sortattr | |
| 1103 | |
| 1104 def filter(self, search_matches, filterspec, sort=[], group=[]): | |
| 767 """Return a list of the ids of the active nodes in this class that | 1105 """Return a list of the ids of the active nodes in this class that |
| 768 match the 'filter' spec, sorted by the group spec and then the | 1106 match the 'filter' spec, sorted by the group spec and then the |
| 769 sort spec. | 1107 sort spec. |
| 770 | 1108 |
| 771 "filterspec" is {propname: value(s)} | 1109 "filterspec" is {propname: value(s)} |
| 772 | 1110 |
| 773 Note that now the propname in filterspec may be transitive, | 1111 "sort" and "group" are [(dir, prop), ...] where dir is '+', '-' |
| 774 i.e., it may contain properties of the form link.link.link.name, | 1112 or None and prop is a prop name or None. Note that for |
| 775 e.g. you can search for all issues where a message was added by | 1113 backward-compatibility reasons a single (dir, prop) tuple is |
| 776 a certain user in the last week with a filterspec of | 1114 also allowed. |
| 777 {'messages.author' : '42', 'messages.creation' : '.-1w;'} | |
| 778 | |
| 779 "sort" and "group" are (dir, prop) where dir is '+', '-' or None | |
| 780 and prop is a prop name or None | |
| 781 | 1115 |
| 782 "search_matches" is {nodeid: marker} | 1116 "search_matches" is {nodeid: marker} |
| 783 | 1117 |
| 784 The filter must match all properties specificed. If the property | 1118 The filter must match all properties specificed. If the property |
| 785 value to match is a list: | 1119 value to match is a list: |
| 786 | 1120 |
| 787 1. String properties must match all elements in the list, and | 1121 1. String properties must match all elements in the list, and |
| 788 2. Other properties must match any of the elements in the list. | 1122 2. Other properties must match any of the elements in the list. |
| 1123 | |
| 1124 Note that now the propname in filterspec and prop in a | |
| 1125 sort/group spec may be transitive, i.e., it may contain | |
| 1126 properties of the form link.link.link.name, e.g. you can search | |
| 1127 for all issues where a message was added by a certain user in | |
| 1128 the last week with a filterspec of | |
| 1129 {'messages.author' : '42', 'messages.creation' : '.-1w;'} | |
| 789 | 1130 |
| 790 Implementation note: | 1131 Implementation note: |
| 791 This implements a non-optimized version of Transitive search | 1132 This implements a non-optimized version of Transitive search |
| 792 using _filter implemented in a backend class. A more efficient | 1133 using _filter implemented in a backend class. A more efficient |
| 793 version can be implemented in the individual backends -- e.g., | 1134 version can be implemented in the individual backends -- e.g., |
| 794 an SQL backen will want to create a single SQL statement and | 1135 an SQL backen will want to create a single SQL statement and |
| 795 override the filter method instead of implementing _filter. | 1136 override the filter method instead of implementing _filter. |
| 796 """ | 1137 """ |
| 797 proptree = self._proptree (filterspec) | 1138 sortattr = self._sortattr(sort = sort, group = group) |
| 798 return self._propsearch (search_matches, proptree, sort, group) | 1139 proptree = self._proptree(filterspec, sortattr) |
| 1140 proptree.search(search_matches) | |
| 1141 return proptree.sort() | |
| 799 | 1142 |
| 800 def count(self): | 1143 def count(self): |
| 801 """Get the number of nodes in this class. | 1144 """Get the number of nodes in this class. |
| 802 | 1145 |
| 803 If the returned integer is 'numnodes', the ids of all the nodes | 1146 If the returned integer is 'numnodes', the ids of all the nodes |
| 811 """Return a dictionary mapping property names to property objects. | 1154 """Return a dictionary mapping property names to property objects. |
| 812 If the "protected" flag is true, we include protected properties - | 1155 If the "protected" flag is true, we include protected properties - |
| 813 those which may not be modified. | 1156 those which may not be modified. |
| 814 """ | 1157 """ |
| 815 raise NotImplementedError | 1158 raise NotImplementedError |
| 1159 | |
| 1160 def get_required_props(self, propnames = []): | |
| 1161 """Return a dict of property names mapping to property objects. | |
| 1162 All properties that have the "required" flag set will be | |
| 1163 returned in addition to all properties in the propnames | |
| 1164 parameter. | |
| 1165 """ | |
| 1166 props = self.getprops(protected = False) | |
| 1167 pdict = dict([(p, props[p]) for p in propnames]) | |
| 1168 pdict.update([(k, v) for k, v in props.iteritems() if v.required]) | |
| 1169 return pdict | |
| 816 | 1170 |
| 817 def addprop(self, **properties): | 1171 def addprop(self, **properties): |
| 818 """Add properties to this class. | 1172 """Add properties to this class. |
| 819 | 1173 |
| 820 The keyword arguments in 'properties' must map names to property | 1174 The keyword arguments in 'properties' must map names to property |
