#!/usr/bin/env python """ This file provides the code generator which transforms gaphor/UML/uml2.gaphor into gaphor/UML/uml2.py. Also a distutils tool, build_uml, is provided. """ # # Create a UML 2.0 datamodel from the Gaphor 0.2.0 model file. # # To do this we do the following: # 1. read the model file with the gaphor parser # 2. Create a object herarcy by ordering elements based on generalizations # Recreate the model using some very dynamic class, so we can set all # attributes and traverse them to generate the data model. from gaphor.parser import parse, base, element, canvas, canvasitem import sys, string, operator import override header = """# This file is generated by build_uml.py. DO NOT EDIT! from properties import association, attribute, enumeration, derivedunion, redefine """ # Make getitem behave more politely base.__real_getitem__ = base.__getitem__ def base__getitem__(self, key): try: return self.__real_getitem__(key) except KeyError: return None base.__getitem__ = base__getitem__ # redefine 'bool' for Python version < 2.3 if map(int, sys.version[:3].split('.')) < [2, 3]: header = header + "bool = int\n" import re pattern = r'([A-Z])' sub = r'_\1' def camelCase_to_underscore(str): """ >>> camelCase_to_underscore('camelcase') 'camelcase' >>> camelCase_to_underscore('camelCase') 'camel_case' >>> camelCase_to_underscore('camelCamelCase') 'camel_camel_case' """ return re.sub(pattern, sub, str).lower() _ = camelCase_to_underscore def msg(s): sys.stderr.write(' ') sys.stderr.write(s) sys.stderr.write('\n') sys.stderr.flush() class Writer: def __init__(self, filename, overridesfile=None): self.overrides = overridesfile and override.Overrides(overridesfile) or None if filename: self.out = open(filename, 'w') else: self.out = sys.stdout def write(self, data): self.out.write(data) def close(self): self.out.close() def write_classdef(self, clazz): """Write a class definition (class xx(x): pass). First the parent classes are examined. After that its own definition is written. It is ensured that class definitions are only written once. """ if not clazz.written: s = '' for g in clazz.generalization: self.write_classdef(g) if s: s += ', ' s = s + g['name'] if not s: s = 'object' if not self.overrides.write_override(self, clazz['name']): self.write('class %s(%s): pass\n' % (clazz['name'], s)) clazz.written = True def write_property(self, full_name, value): """Write a property to the file. If the property is overridden, use the overridden value. full_name should be like Class.attribute. value is free format text. """ if not self.overrides.write_override(self, full_name): self.write('%s = %s\n' % (full_name, value)) def write_attribute(self, a, enumerations={}): """Write a definition for attribute a. Enumerations may be a dict of enumerations, indexed by ID. These are used to identify enums. """ params = { } type = a.typeValue and a.typeValue.get('value') if type is None: raise ValueError('ERROR! type is not specified for property %s.%s' % (a.class_name, a.name)) if type.lower() == 'boolean': # FixMe: Should this be a boolean or an integer? # Integer is save and compattable with python2.2. type = 'int' elif type.lower() in ('integer', 'unlimitednatural'): type = 'int' elif type.lower() == 'string': # Change to basestr for Python 2.3 type = 'str' #type = '(str, unicode)' default = a.defaultValue and a.defaultValue.value # Make sure types are represented the Python way: if default and default.lower() in ('true', 'false'): default = default.title() # True or False... if default is not None: params['default'] = str(default) lower = a.lowerValue and a.lowerValue.value if lower and lower != '0': params['lower'] = lower upper = a.upperValue and a.upperValue.value if upper and upper != '1': params['upper'] = upper #kind, derived, a.name, type, default, lower, upper = parse_attribute(a) full_name = "%s.%s" % (a.class_name, a.name) if self.overrides.has_override(full_name): self.overrides.write_override(self, full_name) elif eval(a.isDerived or '0'): msg('ignoring derived attribute %s.%s: no definition' % (a.class_name, a.name)) elif type.endswith('Kind') or type.endswith('Sort'): e = filter(lambda e: e['name'] == type, enumerations.values())[0] self.write_property("%s.%s" % (a.class_name, a.name), "enumeration('%s', %s, '%s')" % (a.name, e.enumerates, default or e.enumerates[0])) else: if params: attribute = "attribute('%s', %s, %s)" % (a.name, type, ', '.join(map('='.join, params.items()))) else: attribute = "attribute('%s', %s)" % (a.name, type) self.write_property("%s.%s" % (a.class_name, a.name), attribute) def write_operation(self, o): full_name = "%s.%s" % (o.class_name, o.name) if self.overrides.has_override(full_name): self.overrides.write_override(self, full_name) else: msg("No override for operation %s" % full_name) def write_association(self, head, tail): """Write an association for head. False is returned if the association is derived. The head association end is enriched with the following attributes: derived - association is a derived union or not name - name of the association end (name of head is found on tail) class_name - name of the class this association belongs to opposite_class_name - name of the class at the other end of the assoc. lower - lower multiplicity upper - upper multiplicity subsets - derived unions that use the association redefines - redefines existing associations Returns True if the association is written. False is returned if the association should be handled by write_derivedunion() or write_redefine(). """ navigable = head.get('class_') if not navigable: # from this side, the association is not navigable return True name = head.name if name is None: raise ValueError('ERROR! no name, but navigable: %s (%s.%s)' % (head.id, head.class_name, head.name)) derived = int(head.isDerived or 0) #print head.id, head.lowerValue upper = head.upperValue and head.upperValue.value or '*' lower = head.lowerValue and head.lowerValue.value or upper if lower == '*': lower = 0 subsets, redefines = parse_association_tags(head.taggedValue) # Add the values found. These are used later to generate derived unions. head.derived = derived #head.name = name head.class_name = head.class_['name'] head.opposite_class_name = head.type['name'] head.lower = lower head.upper = upper head.subsets = subsets head.redefines = redefines # Derived unions and redefines are handled separately if derived or redefines: return False a = "association('%s', %s" % (name, head.opposite_class_name) if lower not in ('0', 0): a += ', lower=%s' % lower if upper != '*': a += ', upper=%s' % upper if head.get('aggregation') == 'composite': a += ', composite=True' # Add the opposite property if the head itself is navigable: navigable = tail.get('class_') if navigable: try: #o_derived, o_name = parse_association_name(tail['name']) o_name = tail.name o_derived = int(tail.isDerived or 0) except KeyError: msg('ERROR! no name, but navigable: %s (%s.%s)' % (tail.id, tail.class_name, tail.name)) else: assert not (derived and not o_derived), 'One end is derived, the other end not ???' a += ", opposite='%s'" % o_name self.write_property("%s.%s" % (head.class_name, name), a + ')') return True def write_derivedunion(self, d): """Write a derived union. If there are no subsets a warning is issued. The derivedunion is still created though. Derived unions may be created for associations that were returned False by write_association().""" subs = '' for u in d.union: if u.derived and not u.written: self.write_derivedunion(u) if subs: subs += ', ' subs += '%s.%s' % (u.class_name, u.name) if subs: self.write_property("%s.%s" % (d.class_name, d.name), "derivedunion('%s', %s, %s, %s)" % (d.name, d.lower, d.upper == '*' and "'*'" or d.upper, subs)) else: if not self.overrides.has_override('%s.%s' % (d.class_name, d.name)): msg('no subsets for derived union: %s.%s[%s..%s]' % (d.class_name, d.name, d.lower, d.upper)) self.write_property("%s.%s" % (d.class_name, d.name), "derivedunion('%s', %s, %s)" % (d.name, d.lower, d.upper == '*' and "'*'" or d.upper)) d.written = True def write_redefine(self, r): """Redefines may be created for associations that were returned False by write_association().""" self.write_property("%s.%s" % (r.class_name, r.name), "redefine('%s', %s, %s)" % (r.name, r.opposite_class_name, r.redefines)) def parse_association_name(name): # First remove spaces name = name.replace(' ','') derived = False # Check if this is a derived union while name and not name[0].isalpha(): if name[0] == '/': derived = True name = name[1:] return derived, name def parse_association_tags(tag): subsets = [] redefines = None # subsets has a comma separated syntax. Add all taggedValues together if type(tag) is type([]): tag = ', '.join(filter(None, map(getattr, tag, ['value'] * len(tag)))) elif tag: tag = tag.value #print 'scanning tags: %s' % tag if tag and tag.find('subsets') != -1: # find the text after 'subsets': subsets = tag[tag.find('subsets') + len('subsets'):] # remove all whitespaces and stuff subsets = subsets.replace(' ', '').replace('\n', '').replace('\r', '') subsets = subsets.split(',') if tag and tag.find('redefines') != -1: # find the text after 'redefines': redefines = tag[tag.find('redefines') + len('redefines'):] # remove all whitespaces and stuff redefines = redefines.replace(' ', '').replace('\n', '').replace('\r', '') l = redefines.split(',') assert len(l) == 1 redefines = l[0] #print 'found', subsets, redefines return subsets, redefines def generate(filename, outfile=None, overridesfile=None): # parse the file all_elements = parse(filename) def unref(val, attr): """Resolve references. """ try: refs = val.references[attr] except KeyError: val.references[attr] = None return if type(refs) is type([]): unrefs = [] for r in refs: unrefs.append(all_elements[r]) val.references[attr] = unrefs else: val.references[attr] = all_elements[refs] writer = Writer(outfile, overridesfile) # extract usable elements from all_elements. Some elements are given # some extra attributes. classes = { } enumerations = { } generalizations = { } associations = { } properties = { } operations = { } for key, val in all_elements.items(): # Find classes, *Kind (enumerations) are given special treatment if isinstance(val, element): if val.type == 'Class' and val.get('name'): if val['name'].endswith('Kind') or val['name'].endswith('Sort'): enumerations[key] = val else: classes[key] = val # Add extra properties for easy code generation: val.specialization = [] val.generalization = [] val.written = False elif val.type == 'Generalization': generalizations[key] = val elif val.type == 'Association': associations[key] = val elif val.type == 'Property': properties[key] = val unref(val, 'typeValue') unref(val, 'defaultValue') unref(val, 'lowerValue') unref(val, 'upperValue') unref(val, 'taggedValue') elif val.type == 'Operation': operations[key] = val # find inheritance relationships for g in generalizations.values(): #assert g.specific and g.general specific = g['specific'] general = g['general'] classes[specific].generalization.append(classes[general]) classes[general].specialization.append(classes[specific]) # add values to enumerations: for e in enumerations.values(): values = [] for key in e['ownedAttribute']: values.append(str(properties[key]['name'])) e.enumerates = tuple(values) # create file header writer.write(header) # create class definitions for c in classes.values(): writer.write_classdef(c) # create attributes and enumerations for c in classes.values(): for p in c.get('ownedAttribute') or []: a = properties.get(p) # set class_name, since write_attribute depends on it a.class_name = c['name'] if not a.get('association'): writer.write_attribute(a, enumerations) # create associations, derivedunions are held back derivedunions = { } # indexed by name in stead of id redefines = [ ] for a in associations.values(): ends = [] for end in a['memberEnd']: end = properties[end] end.type = classes[end['type']] end.class_ = end.get('class_') and classes[end['class_']] or None #assert end.type is not end.class_ #if not end.get('lowerValue'): #end.lowerValue = end.lowerValue.get('value') or '' #else: #end.lowerValue = '' ends.append(end) for e1, e2 in ((ends[0], ends[1]), (ends[1], ends[0])): if not writer.write_association(e1, e2): # At this point the association is parsed, but not written. # assure that derived unions do not get overwritten if e1.redefines: redefines.append(e1) else: assert not derivedunions.get(e1.name), "%s.%s is already in derived union set in class %s" % (e1.class_name, e1.name, derivedunions.get(e1.name).class_name) derivedunions[e1.name] = e1 e1.union = [ ] e1.written = False # create derived unions, first link the association ends to the d for a in [v for v in properties.values() if v.subsets]: for s in a.subsets or (): try: derivedunions[s].union.append(a) except KeyError: msg('not a derived union: %s.%s' % (a.class_name, s)) for d in derivedunions.values(): writer.write_derivedunion(d) for r in redefines or (): msg('redefining %s -> %s.%s' % (r.redefines, r.class_name, r.name)) writer.write_redefine(r) # create operations for c in classes.values(): for p in c.get('ownedOperation') or []: o = operations.get(p) o.class_name = c['name'] writer.write_operation(o) writer.close() if __name__ == '__main__': import doctest doctest.testmod() generate('uml2.gaphor') # vim:sw=4:et