root/gaphor/tags/gaphor-0.7.0/uml2/properties.py

Revision 445, 19.4 kB (checked in by arjanmol, 4 years ago)

*** empty log message ***

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