Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

root/tags/1.4.0/units.py

Revision 156 (checked in by fumanchu, 3 years ago)

Comments and a "raise x" fix.

  • Property svn:eol-style set to native
Line 
1 try:
2     import cPickle as pickle
3 except ImportError:
4     import pickle
5
6 import sha
7 import types
8 import warnings
9
10 import logic
11 import errors
12
13
14 __all__ = ['UnitAssociation', 'ToMany', 'ToOne', 'UnitJoin',
15            'Unit', 'UnitProperty', 'TriggerProperty', 'MetaUnit',
16            'UnitSequencerInteger', 'UnitSequencerNull',
17            'UnitSequencerUnicode',
18            '_define_fixedpoint_states', '_fix_fixedpoint_cmp',
19            ]
20
21 def _fix_fixedpoint_cmp():
22     """Add methods to fixedpoint to support pickling."""
23     import fixedpoint
24     def __cmp__(self, other):
25         if other is None:
26             return 1
27         xn, yn, p = fixedpoint._norm(self, other, FixedPoint=type(self))
28         return cmp(xn, yn)
29     fixedpoint.FixedPoint.__cmp__ = __cmp__
30
31 def _define_fixedpoint_states():
32     """Add methods to fixedpoint to support pickling."""
33     import fixedpoint
34    
35     if not hasattr(fixedpoint.FixedPoint, "__getstate__"):
36         def __getstate__(self):
37             return (self.n, self.p)
38         fixedpoint.FixedPoint.__getstate__ = __getstate__
39        
40         def __setstate__(self, v):
41             self.n, self.p = v
42         fixedpoint.FixedPoint.__setstate__ = __setstate__
43
44
45 ###########################################################################
46 ##                                                                       ##
47 ##                             Associations                              ##
48 ##                                                                       ##
49 ###########################################################################
50
51
52 class UnitAssociation(object):
53     """Non-data descriptor method to retrieve related Units via attributes."""
54    
55     to_many = None
56    
57     def __init__(self, nearKey, farClass, farKey):
58         # Since the keys will be used as kwarg keys, they must be strings.
59         self.nearKey = str(nearKey)
60         self.farKey = str(farKey)
61        
62         self.nearClass = None
63         self.farClass = farClass
64    
65     def __get__(self, unit, unitclass=None):
66         if unit is None:
67             # When calling on the class instead of an instance...
68             return self
69         else:
70             m = types.MethodType(self.related, unit, unitclass)
71             return m
72    
73     def __delete__(self, unit):
74         raise AttributeError("Unit Associations may not be deleted.")
75    
76     def related(self, unit, expr=None):
77         raise NotImplementedError
78
79
80 class ToOne(UnitAssociation):
81    
82     to_many = False
83    
84     def related(self, unit, expr=None):
85         value = getattr(unit, self.nearKey)
86         if value is None:
87             return None
88        
89         f = logic.filter(**{self.farKey: value})
90         if expr is not None:
91             f += expr
92         units = unit.sandbox.xrecall(self.farClass, f)
93         try:
94             return units.next()
95         except StopIteration:
96             return None
97
98
99 class ToMany(UnitAssociation):
100    
101     to_many = True
102    
103     def related(self, unit, expr=None):
104         value = getattr(unit, self.nearKey)
105         if value is None:
106             return []
107        
108         f = logic.filter(**{self.farKey: value})
109         if expr is not None:
110             f += expr
111         return unit.sandbox.recall(self.farClass, f)
112
113
114 class UnitJoin(object):
115    
116     def __init__(self, class1, class2, leftbiased=None):
117         self.class1 = class1
118         self.class2 = class2
119         self.leftbiased = leftbiased
120        
121         # From http://msdn.microsoft.com/library/en-us/
122         #           dnacc2k/html/acintsql.asp#acintsql_joins
123         # "OUTER JOINs can be nested inside INNER JOINs in a multi-table
124         # join, but INNER JOINs cannot be nested inside OUTER JOINs."
125         if leftbiased is not None:
126             if ((isinstance(class1, UnitJoin) and class1.leftbiased is None)
127                 or (isinstance(class1, UnitJoin) and class1.leftbiased is None)):
128                 warnings.warn("Some StorageManagers cannot nest an INNER "
129                               "JOIN within an OUTER JOIN. Consider rewriting "
130                               "your join tree.", errors.StorageWarning)
131    
132     def __str__(self):
133         if self.leftbiased is None:
134             op = "&"
135         elif self.leftbiased is True:
136             op = "<<"
137         else:
138             op = ">>"
139         if isinstance(self.class1, UnitJoin):
140             name1 = str(self.class1)
141         else:
142             name1 = self.class1.__name__
143         if isinstance(self.class2, UnitJoin):
144             name2 = str(self.class2)
145         else:
146             name2 = self.class2.__name__
147         return "(%s %s %s)" % (name1, op, name2)
148     __repr__ = __str__
149    
150     def __iter__(self):
151         def genclasses():
152             if isinstance(self.class1, UnitJoin):
153                 for cls in iter(self.class1):
154                     yield cls
155             else:
156                 yield self.class1
157             if isinstance(self.class2, UnitJoin):
158                 for cls in iter(self.class2):
159                     yield cls
160             else:
161                 yield self.class2
162         return genclasses()
163    
164     def __lshift__(self, other):
165         if isinstance(other, (MetaUnit, UnitJoin)):
166             return UnitJoin(self, other, leftbiased=True)
167         else:
168             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
169     __rrshift__ = __lshift__
170    
171     def __rshift__(self, other):
172         if isinstance(other, (MetaUnit, UnitJoin)):
173             return UnitJoin(self, other, leftbiased=False)
174         else:
175             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
176     __rlshift__ = __rshift__
177    
178     def __add__(self, other):
179         if isinstance(other, (MetaUnit, UnitJoin)):
180             return UnitJoin(self, other)
181         else:
182             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
183     __and__ = __add__
184    
185     def __radd__(self, other):
186         if isinstance(other, (MetaUnit, UnitJoin)):
187             return UnitJoin(other, self)
188         else:
189             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
190     __rand__ = __radd__
191
192
193 ###########################################################################
194 ##                                                                       ##
195 ##                            Unit Sequencers                            ##
196 ##                                                                       ##
197 ###########################################################################
198
199
200 # All Units must possess at least one UnitProperty which is an identifier.
201 # The sequencing of identifiers depends upon their type and the particular
202 # needs of the class. Pick one of these UnitSequencers to fit your subclass.
203 # When creating new sequencers, you should aim to generate identifiers that
204 # obey the builtin max() and min() functions.
205
206 class UnitSequencerNull(object):
207     """A null sequencer for Unit identifiers. Sequencing will error.
208     
209     In many cases, identifier values simply have no algorithmic sequence;
210     for example, a set of Employee Units might use Social Security
211     Numbers for identifiers (which you should never, ever do ;).
212     
213     In other cases, sequencing will be best handled by custom algorithms
214     within application code; that is, the job of abstracting the sequence
215     logic would not be worth the effort.
216     """
217    
218     def __init__(self, type=unicode):
219         self.type = type
220    
221     def valid_id(self, value):
222         for atom in value:
223             if atom is None:
224                 return False
225         return True
226    
227     def assign(self, unit, sequence):
228         raise StopIteration("No sequence defined.")
229
230
231 class UnitSequencerInteger(object):
232     """A sequencer for Unit identifiers, where id[i+1] == id[i] + 1."""
233    
234     def __init__(self, type=int, initial=1):
235         self.type = type
236         self.initial = initial
237    
238     def valid_id(self, value):
239         return value != (None,) # and value >= self.initial?
240    
241     def assign(self, unit, sequence):
242         newvalue= self.initial
243         if sequence:
244             m = max(sequence)
245             if m != (None,):
246                 newvalue = m[0] + 1
247         unit.identifiers[0].__set__(unit, newvalue)
248
249
250 class UnitSequencerUnicode(object):
251     """UnitSequencerUnicode(type=unicode, width=6,
252         range="abcdefghijklmnopqrstuvwxyz")
253     A sequencer for Unit identifiers, where e.g. next(['abc']) == 'abd'."""
254    
255     def __init__(self, type=unicode, width=6,
256                  range="abcdefghijklmnopqrstuvwxyz"):
257         self.type = type
258         self.width = width
259         self.range = range
260    
261     def valid_id(self, value):
262         return value != (None,)
263    
264     def assign(self, unit, sequence):
265         r = self.range
266         newvalue = r[0] * self.width
267         if sequence:
268             maxid = max(sequence)[0]
269             if len(maxid) != self.width:
270                 raise OverflowError("'%s' is not of width %s." %
271                                     (maxid, self.width))
272             for i in range(self.width - 1, -1, -1):
273                 pos = r.index(maxid[i]) + 1
274                 if pos >= len(r) or pos < 0:
275                     maxid = maxid[:i] + r[0] + maxid[i+1:]
276                 else:
277                     maxid = maxid[:i] + r[pos] + maxid[i+1:]
278                     break
279             else:
280                 raise OverflowError("Next identifier exceeds width %s."
281                                     % self.width)
282             newvalue = maxid
283         unit.identifiers[0].__set__(unit, newvalue)
284
285
286 ###########################################################################
287 ##                                                                       ##
288 ##                                 Units                                 ##
289 ##                                                                       ##
290 ###########################################################################
291
292
293 class UnitProperty(object):
294     """UnitProperty(type=unicode, index=False, hints=None, key=None)
295     Data descriptor for Unit data which will persist in storage.
296     
297     hints: A dictionary which provides named hints to Storage Managers
298         concerning the nature of the data. A common use, for example,
299         is to inform Managers that would usually store unicode strings
300         as strings of length 255, that a particular value should be
301         a larger object; this is done with a 'bytes' mapping, such as:
302         hints = {u'bytes': 0}, where 0 implies no limit. Canonical storage
303         hint names and implementation details may be found in /storage
304         documentation.
305     """
306    
307     def __init__(self, type=unicode, index=False, hints=None, key=None, default=None):
308         if type.__name__ == 'FixedPoint':
309             # fixedpoint can't handle "FixedPoint() != None" in Python 2.4
310             _fix_fixedpoint_cmp()
311            
312             # fixedpoint.Fixedpoint can't be pickled because it
313             # defines __slots__ but not __getstate__. Provide it.
314             _define_fixedpoint_states()
315        
316         self.type = type
317         self.index = index
318         if hints is None: hints = {}
319         self.hints = hints
320         self.key = key
321         self.default = default
322    
323     def __get__(self, unit, unitclass=None):
324         if unit is None:
325             # When calling on the class instead of an instance...
326             return self
327         else:
328             return unit._properties[self.key]
329    
330     def __set__(self, unit, value):
331         if self.coerce:
332             value = self.coerce(unit, value)
333         oldvalue = unit._properties[self.key]
334         if oldvalue != value:
335             unit._properties[self.key] = value
336    
337     def coerce(self, unit, value):
338         if value is not None and not isinstance(value, self.type):
339             # Try to coerce the value.
340             try:
341                 value = self.type(value)
342             except Exception, x:
343                 x.args += (value, type(value))
344                 raise
345         return value
346    
347     def __delete__(self, unit):
348         raise AttributeError("Unit Properties may not be deleted.")
349
350
351 class TriggerProperty(UnitProperty):
352     """UnitProperty subclass for managing immediate triggers on set.
353     
354     The __set__ method will call the on_set method, which should then
355     deal with the new value.
356     """
357    
358     def __set__(self, unit, value):
359         if self.coerce:
360             value = self.coerce(unit, value)
361         oldvalue = unit._properties[self.key]
362         if oldvalue != value:
363             unit._properties[self.key] = value
364             if unit.sandbox:
365                 self.on_set(unit, oldvalue)
366    
367     def on_set(self, unit, oldvalue):
368         pass
369
370
371 class MetaUnit(type):
372    
373     def __init__(cls, name, bases, dct):
374         # Make a copy of the parent class' _associations, and store
375         # it in the _associations attribute of this subclass. In this
376         # manner, Unit Associations should propagate down to subclasses,
377         # but not back up to superclasses.
378         if hasattr(cls, "_associations"):
379             assocs = cls._associations.copy()
380         else:
381             assocs = {}
382        
383         # Make a copy of the parent class' _properties keys, and store
384         # it in the _properties attribute of this subclass. In this
385         # manner, Unit Property keys should propagate down to subclasses,
386         # but not back up to superclasses.
387         if hasattr(cls, "_properties"):
388             props = dict.fromkeys(cls._properties.keys())
389         else:
390             props = {}
391        
392         for name, val in dct.iteritems():
393             # Now grab any new UnitProperties defined in this class.
394             # Overwrite any properties defined in superclasses.
395             if isinstance(val, UnitProperty):
396                 # If the UnitProperty.key is None,
397                 # supply it from the attribute name (name).
398                 if val.key is None:
399                     val.key = name
400                 props[name] = val
401            
402             # Remove any properties from the parent class if requested
403             # (request by binding the parent's UnitProperty.key to None).
404             if name in props and val is None:
405                 del props[name]
406                
407             # Now grab any new UnitAssociations defined in this class.
408             if isinstance(val, UnitAssociation):
409                 val.nearClass = cls
410                 assocs[name] = val
411        
412         cls._properties = props
413         cls._associations = assocs
414    
415     def __lshift__(self, other):
416         if isinstance(other, (MetaUnit, UnitJoin)):
417             return UnitJoin(self, other, leftbiased=True)
418         else:
419             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
420     __rrshift__ = __lshift__
421    
422     def __rshift__(self, other):
423         if isinstance(other, (MetaUnit, UnitJoin)):
424             return UnitJoin(self, other, leftbiased=False)
425         else:
426             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
427     __rlshift__ = __rshift__
428    
429     def __add__(self, other):
430         if isinstance(other, (MetaUnit, UnitJoin)):
431             return UnitJoin(self, other)
432         else:
433             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
434     __and__ = __add__
435    
436     def __radd__(self, other):
437         if isinstance(other, (MetaUnit, UnitJoin)):
438             return UnitJoin(other, self)
439         else:
440             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
441     __rand__ = __radd__
442
443
444 class Unit(object):
445     """Unit(**kwarg properties). A generic, persistent object.
446     
447     Units are the building-block of Dejavu. They are purposefully lightweight,
448     relying on Sandboxes to cache them, which in turn rely on Storage Managers
449     to load and save them.
450     
451     They maintain their own "schema" via UnitProperty objects, so that the
452     Storage Managers don't need to know every detail about every Unit.
453     Storage Managers for simple databases, for example, will simply create
454     a single flat table for each unit type. If you write a custom Storage
455     Manager, you can do as you like; the only place you might run into a
456     problem is if you write a custom Storage Manager for custom Unit types,
457     because the knowledge between the two is indeterminate. For example,
458     if we provide a standard StorageManagerForLotusNotes, and you create
459     custom Units which interface with it, you should probably subclass and
460     extend our StorageManagerForLotusNotes with some custom storage logic.
461     
462     sandbox: The sandbox in which the Unit "lives". Also serves as a flag
463         indicating whether this Unit has finished the initial creation
464         process.
465         
466         Sandboxes receive Units during recall() and memorize();
467         these processes should set the sandbox attribute.
468     
469     dirty: indicates whether elements in the _properties dictionary
470         have been modified. This flag is used by Sandboxes to optimize
471         forget(): they do not ask Storage Managers to save data for Units
472         which have not been modified. Because SM's may cache Units, no code
473         should set this flag other than UnitProperty.__set__ and SM's.
474     """
475    
476     __metaclass__ = MetaUnit
477     _properties = {}
478     _associations = {}
479    
480     # The default ID type is int. If you wish to use a different type for
481     # the ID's of a subclass of Unit, just overwrite ID, e.g.:
482     #     ID = UnitProperty(unicode, index=True)
483     #       or
484     #     UnitSubclass.set_property('ID', unicode, index=True)
485     #       or even
486     #     UnitSubclass.ID.type = unicode
487     ID = UnitProperty(int, index=True)
488     sequencer = UnitSequencerInteger()
489     identifiers = (ID,)
490    
491     def __init__(self, **kwargs):
492         # Copy the class _properties dict into self._properties,
493         # setting each value to the UnitProperty.default.
494         cls = self.__class__
495         self._properties = dict([(k, getattr(cls, k).default)
496                                  for k in cls._properties.keys()])
497        
498         self.sandbox = None
499        
500         # Make sure we cleanse before assigning properties from kwargs,
501         # or the new unit won't get saved if there are no further changes.
502         self.cleanse()
503         for k, v in kwargs.iteritems():
504             setattr(self, k, v)
505    
506     def repress(self):
507         """repress() -> Remove this Unit from memory (do not destroy)."""
508         self.sandbox.repress(self)
509    
510     def forget(self):
511         """forget() -> Destroy this Unit."""
512         self.sandbox.forget(self)
513    
514     def __copy__(self):
515         newUnit = self.__class__()
516         for key in self.__class__.properties():
517             if key not in self.identifiers:
518                 newUnit._properties[key] = self._properties[key]
519         for prop