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 188 (checked in by fumanchu, 7 years ago)

Fix for #48 (unitclass.identifiers should be strings, not property instances).

  • 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, identity):
222         for val in identity:
223             if val 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, identity):
239         return identity != (None,) # and identity[0] >= 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         setattr(unit, unit.identifiers[0], 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, identity):
262         return identity != (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         setattr(unit, unit.identifiers[0], 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         # Keep backward compatibility from 1.4 to 1.5. See ticket #48.
416         ident = dct.get('identifiers', ())
417         if ident:
418             newident = []
419             for val in ident:
420                 if isinstance(val, UnitProperty):
421                     # Substitute the name for the property
422                     val = val.key
423                 newident.append(val)
424             cls.identifiers = tuple(newident)
425    
426     def __lshift__(self, other):
427         if isinstance(other, (MetaUnit, UnitJoin)):
428             return UnitJoin(self, other, leftbiased=True)
429         else:
430             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
431     __rrshift__ = __lshift__
432    
433     def __rshift__(self, other):
434         if isinstance(other, (MetaUnit, UnitJoin)):
435             return UnitJoin(self, other, leftbiased=False)
436         else:
437             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
438     __rlshift__ = __rshift__
439    
440     def __add__(self, other):
441         if isinstance(other, (MetaUnit, UnitJoin)):
442             return UnitJoin(self, other)
443         else:
444             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
445     __and__ = __add__
446    
447     def __radd__(self, other):
448         if isinstance(other, (MetaUnit, UnitJoin)):
449             return UnitJoin(other, self)
450         else:
451             raise TypeError("Joined classes must be UnitJoin or Unit subclasses.")
452     __rand__ = __radd__
453
454
455 class Unit(object):
456     """Unit(**kwarg properties). A generic, persistent object.
457     
458     Units are the building-block of Dejavu. They are purposefully lightweight,
459     relying on Sandboxes to cache them, which in turn rely on Storage Managers
460     to load and save them.
461     
462     They maintain their own "schema" via UnitProperty objects, so that the
463     Storage Managers don't need to know every detail about every Unit.
464     Storage Managers for simple databases, for example, will simply create
465     a single flat table for each unit type. If you write a custom Storage
466     Manager, you can do as you like; the only place you might run into a
467     problem is if you write a custom Storage Manager for custom Unit types,
468     because the knowledge between the two is indeterminate. For example,
469     if we provide a standard StorageManagerForLotusNotes, and you create
470     custom Units which interface with it, you should probably subclass and
471     extend our StorageManagerForLotusNotes with some custom storage logic.
472     
473     sandbox: The sandbox in which the Unit "lives". Also serves as a flag
474         indicating whether this Unit has finished the initial creation
475         process.
476         
477         Sandboxes receive Units during recall() and memorize();
478         these processes should set the sandbox attribute.
479     
480     dirty: indicates whether elements in the _properties dictionary
481         have been modified. This flag is used by Sandboxes to optimize
482         forget(): they do not ask Storage Managers to save data for Units
483         which have not been modified. Because SM's may cache Units, no code
484         should set this flag other than UnitProperty.__set__ and SM's.
485     """
486    
487     __metaclass__ = MetaUnit
488     _properties = {}
489     _associations = {}
490    
491     # The default ID type is int. If you wish to use a different type for
492     # the ID's of a subclass of Unit, just overwrite ID, e.g.:
493     #     ID = UnitProperty(unicode, index=True)
494     #       or
495     #     UnitSubclass.set_property('ID', unicode, index=True)
496     #       or even
497     #     UnitSubclass.ID.type = unicode
498     ID = UnitProperty(int, index=True)
499     sequencer = UnitSequencerInteger()
500     identifiers = ("ID",)
501    
502     def __init__(self, **kwargs):
503         # Copy the class _properties dict into self._properties,
504         # setting each value to the UnitProperty.default.
505         cls = self.__class__
506         self._properties = dict([(k, getattr(cls, k).default)
507                                  for k in cls._properties.keys()])
508        
509         self.sandbox = None
510        
511         # Make sure we cleanse before assigning properties from kwargs,
512         # or the new unit won't get saved if there are no further changes.
513         self.cleanse()
514         for k, v in kwargs.iteritems():
515             setattr(self, k, v)
516    
517     def repress(self):
518         """repress() -> Remove this Unit from memory (do not destroy)."""
519         self.sandbox.repress(self)
520    
521     def forget(self):
522         """forget() -> Destroy this Unit."""
523         self.sandbox.forget(self)
524    
525     def __copy__(self):
526         newUnit = self.__class__()
527         for key, prop in self.__class__._properties.iteritems():
528             if key in self.identifiers:
529                 newUnit._properties[key] = prop.default
530             else:
531                 newUnit._properties[key] = self._properties[key]
532         newUnit.sandbox = None
533         return newUnit
534    
535     #                        Pickle data                         #
536    
537     def __getstate__(self):
538         return (self._properties, self._initial_property_hash)
539    
540     def __setstate__(self, state):
541         self.sandbox = None
542         self._properties, self._initial_property_hash = state
543    
544    
545     #                         Properties                         #
546    
547     def identity(self):
548         # Must be immutable for use as a dictionary key.
549         return tuple([getattr(self, key) for key in self.identifiers])
550    
551     def _property_hash(self):
552         try:
553             return sha.new(pickle.dumps(self._properties)).digest()
554         except TypeError, x:
555             x.args += (self.__class__.__name__, self._properties.keys())
556             raise
557    
558     def dirty(self):
559         return self._initial_property_hash != self._property_hash()
560    
561     def cleanse(self):
562         self._initial_property_hash = self._property_hash()
563    
564     def set_property(cls, key, type=unicode, index=False,
565                      descriptor=UnitProperty):
566         """Set a Unit Property for cls."""
567         setattr(cls, key, descriptor(type, index, key=key))
568         cls._properties[key] = None
569     set_property = classmethod(set_property)
570    
571     def set_properties(cls, types={}, descriptor=UnitProperty):
572         """Set Unit Properties for cls."""
573         for key, typ in types.items():
574             cls.set_property(key, typ, False, descriptor)
575     set_properties = classmethod(set_properties)
576    
577     def remove_property(cls, key):
578         delattr(cls, key)
579         del cls._properties[key]
580     remove_property = classmethod(remove_property)
581    
582     def indices(cls):
583         """cls.indices() -> tuple of names of indexed UnitProperties."""
584         product = []
585         for key in cls.properties():
586             try:
587                 if getattr(cls, key).index:
588                     product.append(key)
589             except AttributeError, x:
590                 x.args += (cls, key)
591                 raise
592         return tuple(product)
593     indices = classmethod(indices)
594    
595     def properties(cls):
596         """cls.properties() -> list of UnitProperty names."""
597         return cls._properties.iterkeys()
598     properties = classmethod(properties)
599    
600     def property_type(cls, key):
601         """cls.property_type(key) -> type of the given UnitProperty."""
602         # Retrieving from the class gives us
603         # the UnitProperty object, not its value.
604         return getattr(cls, key).type
605     property_type = classmethod(property_type)
606    
607     def adjust(self, **values):
608         """adjust(**values) -> Set UnitProperties by key, value pairs."""
609         for key, val in values.iteritems():
610             setattr(self, key, val)
611    
612    
613     #                        Associations                        #
614    
615     def associate(nearClass, nearKey, farClass, farKey, nearDescriptor, farDescriptor):
616         """Set UnitAssociations between nearClass.key and farClass.farKey."""
617         # Mangle this class first
618         farClassName = farClass.__name__
619         descriptor = nearDescriptor(nearKey, farClass, farKey)
620         descriptor.nearClass = nearClass
621         setattr(nearClass, farClassName, descriptor)
622         nearClass._associations[farClassName] = descriptor
623        
624         # Now mangle the far class
625         nearClassName = nearClass.__name__
626         descriptor = farDescriptor(farKey, nearClass, nearKey)
627         descriptor.nearClass = farClass
628         setattr(farClass, nearClassName, descriptor)
629         farClass._associations[nearClassName] = descriptor
630     associate = classmethod(associate)
631    
632     def one_to_many(nearClass, nearKey, farClass, farKey):
633         nearClass.associate(nearKey, farClass, farKey, ToMany, ToOne)
634     one_to_many = classmethod(one_to_many)
635    
636     def one_to_one(nearClass, nearKey, farClass, farKey):
637         nearClass.associate(nearKey, farClass, farKey, ToOne, ToOne)
638     one_to_one = classmethod(one_to_one)
639    
640     def many_to_one(nearClass, nearKey, farClass, farKey):
641         nearClass.associate(nearKey, farClass, farKey, ToOne, ToMany)
642     many_to_one = classmethod(many_to_one)
643    
644     def associations(cls):
645         """cls.associations() -> list of UnitAssociation names."""
646         return cls._associations.iterkeys()
647     associations = classmethod(associations)
648    
649     def add(self, *units):
650         """add(*units) -> Auto-create a relation between self and unit(s)."""
651         cls = self.__class__
652         for unit in units:
653             try:
654                 ua = cls._associations[unit.__class__.__name__]
655             except KeyError:
656                 msg = "'%s' is not associated with '%s'" % (cls, unit.__class__)
657                 raise errors.AssociationError(msg)
658            
659             nearval = getattr(self, ua.nearKey)
660             farval = getattr(unit, ua.farKey)
661             if nearval is None:
662                 if farval is None:
663                     raise errors.AssociationError("At least one Unit key must be set.")
664                 else:
665                     setattr(self, ua.nearKey, farval)
666             else:
667                 # If far key is already set, it will simply be overwritten.
668                 setattr(unit, ua.farKey, nearval)
Note: See TracBrowser for help on using the browser.