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

UnitJoin? instances may now be compared for equality.

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