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

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