Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

root/trunk/units.py

Revision 289 (checked in by fumanchu, 7 years ago)

Made some relative imports absolute for Python 2.5 (just in case).

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