Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

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

root/trunk/dejavu/units.py

Revision 605 (checked in by lakin, 1 year ago)

use hashlib instead of sha when it's available, but fallback to the default sha module.

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