root/gaphor/tags/gaphor-0.12.0/gaphor/UML/properties.py

Revision 2091, 20.9 kB (checked in by arj..@yirdis.nl, 1 year ago)

Unlink composite associations through a direct call to the umlproperty. This is step 1 that should eventually depricate the Element.connect/disconnect logic.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 """
2 Properties used to create the UML 2.0 data model.
3
4 The logic for creating and destroying connections between UML objects is
5 implemented in Python property classes. These classes are simply instantiated
6 like this:
7     class Class(Element): pass
8     class Comment(Element): pass
9     Class.ownedComment = association('ownedComment', Comment,
10                                      0, '*', 'annotatedElement')
11     Comment.annotatedElement = association('annotatedElement', Element,
12                                            0, '*', 'ownedComment')
13
14 Same for attributes and enumerations.
15
16 Each property type (association, attribute and enumeration) has three specific
17 methods:
18     _get():           return the value
19     _set(value):      set the value or add it to a list
20     _del(value=None): delete the value. 'value' is used to tell which value
21                       is to be removed (in case of associations with
22                       multiplicity > 1).
23     load(value):      load 'value' as the current value for this property
24     save(save_func):  send the value of the property to save_func(name, value)
25 """
26
27 __all__ = [ 'attribute', 'enumeration', 'association', 'derivedunion', 'redefine' ]
28
29 from zope import component
30 from collection import collection
31 from event import AttributeChangeEvent, AssociationSetEvent, \
32                   AssociationAddEvent, AssociationDeleteEvent
33 from event import DerivedUnionSetEvent, DerivedUnionAddEvent, \
34                   DerivedUnionDeleteEvent
35 from event import RedefineSetEvent, RedefineAddEvent, RedefineDeleteEvent
36 from interfaces import IAssociationChangeEvent, IAssociationSetEvent, \
37                        IAssociationAddEvent, IAssociationDeleteEvent
38 import operator
39
40
41 class umlproperty(object):
42     """
43     Superclass for attribute, enumeration and association.
44     The subclasses should define a 'name' attribute that contains the name
45     of the property. Derived properties (derivedunion and redefine) can be
46     connected, they will be notified when the value changes.
47     """
48
49     def __get__(self, obj, class_=None):
50         if obj:
51             return self._get(obj)
52         return self
53
54     def __set__(self, obj, value):
55         self._set(obj, value)
56
57     def __delete__(self, obj, value=None):
58         self._del(obj, value)
59
60     def save(self, obj, save_func):
61         if hasattr(obj, self._name):
62             save_func(self.name, self._get(obj))
63
64     def load(self, obj, value):
65         self._set(obj, value)
66
67     def postload(self, obj):
68         pass
69
70     def unlink(self, obj):
71         """
72         This is called from the Element to denote the element is unlinking.
73         """
74         pass
75
76     def notify(self, obj):
77         """
78         Notify obj that the property's value has been changed.
79         Deriviates are also triggered to send a notify signal.
80         """
81         try:
82             obj.notify(self.name, pspec=self)
83         except Exception, e:
84             log.error(str(e), e)
85
86
87 class attribute(umlproperty):
88     """
89     Attribute.
90
91     Element.attr = attribute('attr', types.StringType, '')
92     """
93
94     # TODO: check if lower and upper are actually needed for attributes
95     def __init__(self, name, type, default=None, lower=0, upper=1):
96         self.name = intern(name)
97         self._name = intern('_' + name)
98         self.type = type
99         self.default = default
100         self.lower = lower
101         self.upper = upper
102        
103     def load(self, obj, value):
104         # FixMe: value might be a string while some other type is required:
105         #print 'attribute.load:', self.name, self.type, value,
106         if self.type is not object:
107             value = self.type(value)
108         setattr(obj, self._name, value)
109
110     def __str__(self):
111         if self.lower == self.upper:
112             return '<attribute %s: %s[%s] = %s>' % (self.name, self.type, self.lower, self.default)
113         else:
114             return '<attribute %s: %s[%s..%s] = %s>' % (self.name, self.type, self.lower, self.upper, self.default)
115
116     def _get(self, obj):
117         try:
118             return getattr(obj, self._name)
119         except AttributeError:
120             return self.default
121
122     def _set(self, obj, value):
123         if value is not None and not isinstance(value, self.type):
124             raise AttributeError, 'Value should be of type %s' % hasattr(self.type, '__name__') and self.type.__name__ or self.type
125
126         if value == self._get(obj):
127             return
128
129         #undoattributeaction(self, obj, self._get(obj))
130
131         old = self._get(obj)
132         if value == self.default and hasattr(obj, self._name):
133             delattr(obj, self._name)
134         else:
135             setattr(obj, self._name, value)
136         component.handle(AttributeChangeEvent(obj, self, old, value))
137         self.notify(obj)
138
139     def _del(self, obj, value=None):
140         old = self._get(obj)
141         try:
142             #undoattributeaction(self, obj, self._get(obj))
143             delattr(obj, self._name)
144         except AttributeError:
145             pass
146         else:
147             component.handle(AttributeChangeEvent(obj, self, old, self.default))
148             self.notify(obj)
149
150
151 class enumeration(umlproperty):
152     """
153     Enumeration.
154     Element.enum = enumeration('enum', ('one', 'two', 'three'), 'one')
155     """
156
157     def __init__(self, name, values, default):
158         self.name = intern(name)
159         self._name = intern('_' + name)
160         self.values = values
161         self.default = default
162
163     def __str__(self):
164         return '<enumeration %s: %s = %s>' % (self.name, self.values, self.default)
165
166     def _get(self, obj):
167         try:
168             return getattr(obj, self._name)
169         except AttributeError:
170             return self.default
171
172     def load(self, obj, value):
173         if not value in self.values:
174             raise AttributeError, 'Value should be one of %s' % str(self.values)
175         setattr(obj, self._name, value)
176
177     def _set(self, obj, value):
178         if not value in self.values:
179             raise AttributeError, 'Value should be one of %s' % str(self.values)
180         old = self._get(obj)
181         if value == old:
182             return
183
184         if value == self.default:
185             delattr(obj, self._name)
186         else:
187             setattr(obj, self._name, value)
188         component.handle(AttributeChangeEvent(obj, self, old, value))
189         self.notify(obj)
190
191     def _del(self, obj, value=None):
192         old = self._get(obj)
193         try:
194             delattr(obj, self._name)
195         except AttributeError:
196             pass
197         else:
198             component.handle(AttributeChangeEvent(obj, self, old, self.default))
199             self.notify(obj)
200
201
202 class association(umlproperty):
203     """
204     Association, both uni- and bi-directional.
205
206     Element.assoc = association('assoc', Element, opposite='other')
207     
208     A listerer is connected to the value added to the association. This
209     will cause the association to be ended if the element on the other end
210     of the association is unlinked.
211
212     If the association is a composite relationship, the association will
213     unlink all elements attached to if it is unlinked.
214     """
215  
216     def __init__(self, name, type, lower=0, upper='*', composite=False, opposite=None):
217         self.name = intern(name)
218         self._name = intern('_' + name)
219         self.type = type
220         self.lower = lower
221         self.upper = upper
222         self.composite = composite
223         self.opposite = opposite and intern(opposite)
224
225     def load(self, obj, value):
226         if not isinstance(value, self.type):
227             raise AttributeError, 'Value for %s should be of type %s (%s)' % (self.name, self.type.__name__, type(value).__name__)
228         self._set(obj, value, do_notify=False)
229
230     def postload(self, obj):
231         """
232         In the postload step, ensure that bi-directional associations
233         are bi-directional.
234         """
235         values = self._get(obj)
236         if not values:
237             return
238         if self.upper == 1:
239             values = [ values ]
240         for value in values:
241             if not isinstance(value, self.type):
242                 raise AttributeError, 'Error in postload validation for %s: Value %s should be of type %s' % (self.name, value, self.type.__name__)
243
244     def __str__(self):
245         if self.lower == self.upper:
246             s = '<association %s: %s[%s]' % (self.name, self.type.__name__, self.lower)
247         else:
248             s = '<association %s: %s[%s..%s]' % (self.name, self.type.__name__, self.lower, self.upper)
249         if self.opposite:
250             s += ' %s-> %s' % (self.composite and '<>' or '', self.opposite)
251         return s + '>'
252
253     def _get(self, obj):
254         #print '_get', self, obj
255         # TODO: Handle lower and add items if lower > 0
256         try:
257             return getattr(obj, self._name)
258         except AttributeError:
259             if self.upper > 1:
260                 # Create the empty collection here since it might be used to
261                 # add
262                 c = collection(self, obj, self.type)
263                 setattr(obj, self._name, c)
264                 return c
265             else:
266                 return None
267
268     def _set(self, obj, value, from_opposite=False, do_notify=True):
269         """
270         Set a new value for our attribute. If this is a collection, append
271         to the existing collection.
272
273         This method is called from the opposite association property.
274         """
275         #print '__set__', self, obj, value, self._get(obj)
276         if not (isinstance(value, self.type) or \
277                 (value is None and self.upper == 1)):
278             raise AttributeError, 'Value should be of type %s' % self.type.__name__
279         # Remove old value only for uni-directional associations
280         if self.upper == 1:
281             old = self._get(obj)
282
283             # do nothing if we are assigned our current value:
284             # Still do your thing, since undo handlers expect that.
285             if value is old:
286                 return
287
288             if old:
289                 self._del(obj, old, from_opposite=from_opposite, do_notify=False)
290
291             if do_notify:
292                 event = AssociationSetEvent(obj, self, old, value)
293
294             if value is None:
295                 if do_notify:
296                     self.notify(obj)
297                     component.handle(event)
298                 return
299
300             setattr(obj, self._name, value)
301
302         else:
303             # Set the actual value
304             c = self._get(obj)
305             if not c:
306                 c = collection(self, obj, self.type)
307                 setattr(obj, self._name, c)
308             elif value in c:
309                 return
310
311             c.items.append(value)
312             if do_notify:
313                 event = AssociationAddEvent(obj, self, value)
314
315         # Callbacks are only connected if a new relationship has
316         # been established.
317         value.connect('__unlink__', self.__on_unlink, obj)
318 #        if self.composite:
319 #            obj.connect('__unlink__', self.__on_composite_unlink, value)
320        
321         if not from_opposite and self.opposite:
322             getattr(type(value), self.opposite)._set(value, obj, from_opposite=True, do_notify=do_notify)
323
324         if do_notify:
325             self.notify(obj)
326             component.handle(event)
327
328     def _del(self, obj, value, from_opposite=False, do_notify=True):
329         """
330         Delete is used for element deletion and for removal of
331         elements from a list.
332         """
333         #print '__delete__', self, obj, value
334
335         if not value:
336             if self.upper > 1:
337                 raise Exception, 'Can not delete collections'
338             old = value = self._get(obj)
339             if value is None:
340                 return
341
342         if not from_opposite and self.opposite:
343             getattr(type(value), self.opposite)._del(value, obj, from_opposite=True)
344
345         event = None
346         if self.upper > 1:
347             c = self._get(obj)
348             if c:
349                 items = c.items
350                 try:
351                     items.remove(value)
352                 except:
353                     pass
354                 else:
355                     if do_notify:
356                         event = AssociationDeleteEvent(obj, self, value)
357
358                 # Remove items collection if empty
359                 if not items:
360                     delattr(obj, self._name)
361         else:
362             try:
363                 delattr(obj, self._name)
364             except:
365                 pass
366             else:
367                 if do_notify:
368                     event = AssociationSetEvent(obj, self, value, None)
369
370         value.disconnect(self.__on_unlink, obj)
371 #        if self.composite:
372 #            obj.disconnect(self.__on_composite_unlink, value)
373         if do_notify and event:
374             self.notify(obj)
375             component.handle(event)
376
377     def unlink(self, obj):
378         values = self._get(obj)
379         composite = self.composite
380         if values:
381             if self.upper == 1:
382                 values = [values]
383
384             for value in list(values):
385                 # TODO: make normal unlinking work through this method.
386                 #self.__delete__(obj, value)
387                 if composite:
388                     value.unlink()
389
390     def __on_unlink(self, value, pspec, obj):
391         """
392         Disconnect when the element on the other end of the association
393         (value) sends the '__unlink__' signal. This is especially important
394         for uni-directional associations.
395         """
396         #print '__on_unlink', name, obj, value
397         if pspec == '__unlink__':
398             self.__delete__(obj, value)
399             # re-establish unlink handler:
400             value.connect('__unlink__', self.__on_unlink, obj)
401
402
403 class derivedunion(umlproperty):
404     """
405     Derived union
406
407       Element.union = derivedunion('union', subset1, subset2..subsetn)
408
409     The subsets are the properties that participate in the union (Element.name).
410     """
411
412     def __init__(self, name, lower, upper, *subsets):
413         self.name = intern(name)
414         self._name = intern('_' + name)
415         self.lower = lower
416         self.upper = upper
417         self.subsets = set(subsets)
418         self.single = len(subsets) == 1
419
420         component.provideHandler(self._association_changed)
421
422
423     def load(self, obj, value):
424         raise ValueError, 'Derivedunion: Properties should not be loaded in a derived union %s: %s' % (self.name, value)
425
426
427     def save(self, obj, save_func):
428         pass
429
430
431     def __str__(self):
432         return '<derivedunion %s: %s>' % (self.name, str(map(str, self.subsets))[1:-1])
433
434     def _union(self, obj, exclude=None):
435         """
436         Returns a union of all values as a set.
437         """
438         if self.single:
439             return iter(self.subsets).next().__get__(obj)
440         else:
441             u = set()
442             for s in self.subsets:
443                 if s is exclude:
444                     continue
445                 tmp = s.__get__(obj)
446                 if tmp:
447                     try:
448                         u.update(tmp)
449                     except TypeError:
450                         # [0..1] property
451                         u.add(tmp)
452             return u
453
454     def _get(self, obj):
455         u = self._union(obj)
456         if self.upper > 1:
457             return u
458         else:
459             assert len(u) <= 1, 'Derived union %s of item %s should have length 1 %s' % (self.name, obj.id, tuple(u))
460             return u and iter(u).next() or None
461
462
463     def _set(self, obj, value):
464         raise AttributeError, 'Can not set values on a union'
465
466
467     def _del(self, obj, value=None):
468         raise AttributeError, 'Can not delete values on a union'
469
470
471     @component.adapter(IAssociationChangeEvent)
472     def _association_changed(self, event):
473         """
474         Re-emit state change for the derived union (as DerivedUnion*Event's).
475
476         TODO: We should fetch the old and new state of the namespace item in
477         stead of the old and new values of the item that changed.
478
479         If multiplicity is [0..1]:
480           send DerivedUnionSetEvent if len(union) < 2
481         if multiplicity is [*]:
482           send DerivedUnionAddEvent and DerivedUnionDeleteEvent
483             if value not in derived union and
484         """
485         if event.property in self.subsets:
486             # mimic the events for Set/Add/Delete
487
488             if self