| OLD | NEW |
| 1 # Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE). | 1 # Copyright (c) 2004-2010 LOGILAB S.A. (Paris, FRANCE). |
| 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr | 2 # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # | 3 # |
| 4 # This program is free software; you can redistribute it and/or modify it under | 4 # This program is free software; you can redistribute it and/or modify it under |
| 5 # the terms of the GNU General Public License as published by the Free Software | 5 # the terms of the GNU General Public License as published by the Free Software |
| 6 # Foundation; either version 2 of the License, or (at your option) any later | 6 # Foundation; either version 2 of the License, or (at your option) any later |
| 7 # version. | 7 # version. |
| 8 # | 8 # |
| 9 # This program is distributed in the hope that it will be useful, but WITHOUT | 9 # This program is distributed in the hope that it will be useful, but WITHOUT |
| 10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | 10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. | 11 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
| 12 # | 12 # |
| 13 # You should have received a copy of the GNU General Public License along with | 13 # You should have received a copy of the GNU General Public License along with |
| 14 # this program; if not, write to the Free Software Foundation, Inc., | 14 # this program; if not, write to the Free Software Foundation, Inc., |
| 15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 15 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 16 """diagram objects | 16 """diagram objects |
| 17 """ | 17 """ |
| 18 | 18 |
| 19 import astroid | 19 from logilab import astng |
| 20 from pylint.pyreverse.utils import is_interface, FilterMixIn | 20 from pylint.pyreverse.utils import is_interface, FilterMixIn |
| 21 | 21 |
| 22 class Figure(object): | 22 def set_counter(value): |
| 23 """Figure counter (re)set""" |
| 24 Figure._UID_COUNT = value |
| 25 |
| 26 class Figure: |
| 23 """base class for counter handling""" | 27 """base class for counter handling""" |
| 24 | 28 _UID_COUNT = 0 |
| 29 def __init__(self): |
| 30 Figure._UID_COUNT += 1 |
| 31 self.fig_id = Figure._UID_COUNT |
| 32 |
| 25 class Relationship(Figure): | 33 class Relationship(Figure): |
| 26 """a relation ship from an object in the diagram to another | 34 """a relation ship from an object in the diagram to another |
| 27 """ | 35 """ |
| 28 def __init__(self, from_object, to_object, relation_type, name=None): | 36 def __init__(self, from_object, to_object, relation_type, name=None): |
| 29 Figure.__init__(self) | 37 Figure.__init__(self) |
| 30 self.from_object = from_object | 38 self.from_object = from_object |
| 31 self.to_object = to_object | 39 self.to_object = to_object |
| 32 self.type = relation_type | 40 self.type = relation_type |
| 33 self.name = name | 41 self.name = name |
| 34 | 42 |
| 35 | 43 |
| 36 class DiagramEntity(Figure): | 44 class DiagramEntity(Figure): |
| 37 """a diagram object, i.e. a label associated to an astroid node | 45 """a diagram object, i.e. a label associated to an astng node |
| 38 """ | 46 """ |
| 39 def __init__(self, title='No name', node=None): | 47 def __init__(self, title='No name', node=None): |
| 40 Figure.__init__(self) | 48 Figure.__init__(self) |
| 41 self.title = title | 49 self.title = title |
| 42 self.node = node | 50 self.node = node |
| 43 | 51 |
| 44 class ClassDiagram(Figure, FilterMixIn): | 52 class ClassDiagram(Figure, FilterMixIn): |
| 45 """main class diagram handling | 53 """main class diagram handling |
| 46 """ | 54 """ |
| 47 TYPE = 'class' | 55 TYPE = 'class' |
| 48 def __init__(self, title, mode): | 56 def __init__(self, title, mode): |
| 49 FilterMixIn.__init__(self, mode) | 57 FilterMixIn.__init__(self, mode) |
| 50 Figure.__init__(self) | 58 Figure.__init__(self) |
| 51 self.title = title | 59 self.title = title |
| 52 self.objects = [] | 60 self.objects = [] |
| 53 self.relationships = {} | 61 self.relationships = {} |
| 54 self._nodes = {} | 62 self._nodes = {} |
| 55 self.depends = [] | 63 self.depends = [] |
| 56 | 64 |
| 57 def get_relationships(self, role): | 65 def add_relationship(self, from_object, to_object, |
| 58 # sorted to get predictable (hence testable) results | |
| 59 return sorted(self.relationships.get(role, ()), | |
| 60 key=lambda x: (x.from_object.fig_id, x.to_object.fig_id)) | |
| 61 | |
| 62 def add_relationship(self, from_object, to_object, | |
| 63 relation_type, name=None): | 66 relation_type, name=None): |
| 64 """create a relation ship | 67 """create a relation ship |
| 65 """ | 68 """ |
| 66 rel = Relationship(from_object, to_object, relation_type, name) | 69 rel = Relationship(from_object, to_object, relation_type, name) |
| 67 self.relationships.setdefault(relation_type, []).append(rel) | 70 self.relationships.setdefault(relation_type, []).append(rel) |
| 68 | 71 |
| 69 def get_relationship(self, from_object, relation_type): | 72 def get_relationship(self, from_object, relation_type): |
| 70 """return a relation ship or None | 73 """return a relation ship or None |
| 71 """ | 74 """ |
| 72 for rel in self.relationships.get(relation_type, ()): | 75 for rel in self.relationships.get(relation_type, ()): |
| 73 if rel.from_object is from_object: | 76 if rel.from_object is from_object: |
| 74 return rel | 77 return rel |
| 75 raise KeyError(relation_type) | 78 raise KeyError(relation_type) |
| 76 | 79 |
| 77 def get_attrs(self, node): | 80 def get_attrs(self, node): |
| 78 """return visible attributes, possibly with class name""" | 81 """return visible attributes, possibly with class name""" |
| 79 attrs = [] | 82 attrs = [] |
| 80 for node_name, ass_nodes in node.instance_attrs_type.items() + \ | 83 for node_name, ass_nodes in node.instance_attrs_type.items() + \ |
| 81 node.locals_type.items(): | 84 node.locals_type.items(): |
| 82 if not self.show_attr(node_name): | 85 if not self.show_attr(node_name): |
| 83 continue | 86 continue |
| 84 names = self.class_names(ass_nodes) | 87 names = self.class_names(ass_nodes) |
| 85 if names: | 88 if names: |
| 86 node_name = "%s : %s" % (node_name, ", ".join(names)) | 89 node_name = "%s : %s" % (node_name, ", ".join(names)) |
| 87 attrs.append(node_name) | 90 attrs.append(node_name) |
| 88 return sorted(attrs) | 91 return attrs |
| 89 | 92 |
| 90 def get_methods(self, node): | 93 def get_methods(self, node): |
| 91 """return visible methods""" | 94 """return visible methods""" |
| 92 methods = [ | 95 return [m for m in node.values() |
| 93 m for m in node.values() | 96 if isinstance(m, astng.Function) and self.show_attr(m.name)] |
| 94 if isinstance(m, astroid.Function) and self.show_attr(m.name) | |
| 95 ] | |
| 96 return sorted(methods, key=lambda n: n.name) | |
| 97 | 97 |
| 98 def add_object(self, title, node): | 98 def add_object(self, title, node): |
| 99 """create a diagram object | 99 """create a diagram object |
| 100 """ | 100 """ |
| 101 assert node not in self._nodes | 101 assert node not in self._nodes |
| 102 ent = DiagramEntity(title, node) | 102 ent = DiagramEntity(title, node) |
| 103 self._nodes[node] = ent | 103 self._nodes[node] = ent |
| 104 self.objects.append(ent) | 104 self.objects.append(ent) |
| 105 | 105 |
| 106 def class_names(self, nodes): | 106 def class_names(self, nodes): |
| 107 """return class names if needed in diagram""" | 107 """return class names if needed in diagram""" |
| 108 names = [] | 108 names = [] |
| 109 for ass_node in nodes: | 109 for ass_node in nodes: |
| 110 if isinstance(ass_node, astroid.Instance): | 110 if isinstance(ass_node, astng.Instance): |
| 111 ass_node = ass_node._proxied | 111 ass_node = ass_node._proxied |
| 112 if isinstance(ass_node, astroid.Class) \ | 112 if isinstance(ass_node, astng.Class) \ |
| 113 and hasattr(ass_node, "name") and not self.has_node(ass_node): | 113 and hasattr(ass_node, "name") and not self.has_node(ass_node): |
| 114 if ass_node.name not in names: | 114 if ass_node.name not in names: |
| 115 ass_name = ass_node.name | 115 ass_name = ass_node.name |
| 116 names.append(ass_name) | 116 names.append(ass_name) |
| 117 return names | 117 return names |
| 118 | 118 |
| 119 def nodes(self): | 119 def nodes(self): |
| 120 """return the list of underlying nodes | 120 """return the list of underlying nodes |
| 121 """ | 121 """ |
| 122 return self._nodes.keys() | 122 return self._nodes.keys() |
| 123 | 123 |
| 124 def has_node(self, node): | 124 def has_node(self, node): |
| 125 """return true if the given node is included in the diagram | 125 """return true if the given node is included in the diagram |
| 126 """ | 126 """ |
| 127 return node in self._nodes | 127 return node in self._nodes |
| 128 | 128 |
| 129 def object_from_node(self, node): | 129 def object_from_node(self, node): |
| 130 """return the diagram object mapped to node | 130 """return the diagram object mapped to node |
| 131 """ | 131 """ |
| 132 return self._nodes[node] | 132 return self._nodes[node] |
| 133 | 133 |
| 134 def classes(self): | 134 def classes(self): |
| 135 """return all class nodes in the diagram""" | 135 """return all class nodes in the diagram""" |
| 136 return [o for o in self.objects if isinstance(o.node, astroid.Class)] | 136 return [o for o in self.objects if isinstance(o.node, astng.Class)] |
| 137 | 137 |
| 138 def classe(self, name): | 138 def classe(self, name): |
| 139 """return a class by its name, raise KeyError if not found | 139 """return a class by its name, raise KeyError if not found |
| 140 """ | 140 """ |
| 141 for klass in self.classes(): | 141 for klass in self.classes(): |
| 142 if klass.node.name == name: | 142 if klass.node.name == name: |
| 143 return klass | 143 return klass |
| 144 raise KeyError(name) | 144 raise KeyError(name) |
| 145 | 145 |
| 146 def extract_relationships(self): | 146 def extract_relationships(self): |
| 147 """extract relation ships between nodes in the diagram | 147 """extract relation ships between nodes in the diagram |
| 148 """ | 148 """ |
| 149 for obj in self.classes(): | 149 for obj in self.classes(): |
| 150 node = obj.node | 150 node = obj.node |
| 151 obj.attrs = self.get_attrs(node) | 151 obj.attrs = self.get_attrs(node) |
| 152 obj.methods = self.get_methods(node) | 152 obj.methods = self.get_methods(node) |
| 153 # shape | 153 # shape |
| 154 if is_interface(node): | 154 if is_interface(node): |
| 155 obj.shape = 'interface' | 155 obj.shape = 'interface' |
| (...skipping 10 matching lines...) Expand all Loading... |
| 166 for impl_node in node.implements: | 166 for impl_node in node.implements: |
| 167 try: | 167 try: |
| 168 impl_obj = self.object_from_node(impl_node) | 168 impl_obj = self.object_from_node(impl_node) |
| 169 self.add_relationship(obj, impl_obj, 'implements') | 169 self.add_relationship(obj, impl_obj, 'implements') |
| 170 except KeyError: | 170 except KeyError: |
| 171 continue | 171 continue |
| 172 # associations link | 172 # associations link |
| 173 for name, values in node.instance_attrs_type.items() + \ | 173 for name, values in node.instance_attrs_type.items() + \ |
| 174 node.locals_type.items(): | 174 node.locals_type.items(): |
| 175 for value in values: | 175 for value in values: |
| 176 if value is astroid.YES: | 176 if value is astng.YES: |
| 177 continue | 177 continue |
| 178 if isinstance(value, astroid.Instance): | 178 if isinstance( value, astng.Instance): |
| 179 value = value._proxied | 179 value = value._proxied |
| 180 try: | 180 try: |
| 181 ass_obj = self.object_from_node(value) | 181 ass_obj = self.object_from_node(value) |
| 182 self.add_relationship(ass_obj, obj, 'association', name) | 182 self.add_relationship(ass_obj, obj, 'association', name) |
| 183 except KeyError: | 183 except KeyError: |
| 184 continue | 184 continue |
| 185 | 185 |
| 186 | 186 |
| 187 class PackageDiagram(ClassDiagram): | 187 class PackageDiagram(ClassDiagram): |
| 188 """package diagram handling | 188 """package diagram handling |
| 189 """ | 189 """ |
| 190 TYPE = 'package' | 190 TYPE = 'package' |
| 191 | 191 |
| 192 def modules(self): | 192 def modules(self): |
| 193 """return all module nodes in the diagram""" | 193 """return all module nodes in the diagram""" |
| 194 return [o for o in self.objects if isinstance(o.node, astroid.Module)] | 194 return [o for o in self.objects if isinstance(o.node, astng.Module)] |
| 195 | 195 |
| 196 def module(self, name): | 196 def module(self, name): |
| 197 """return a module by its name, raise KeyError if not found | 197 """return a module by its name, raise KeyError if not found |
| 198 """ | 198 """ |
| 199 for mod in self.modules(): | 199 for mod in self.modules(): |
| 200 if mod.node.name == name: | 200 if mod.node.name == name: |
| 201 return mod | 201 return mod |
| 202 raise KeyError(name) | 202 raise KeyError(name) |
| 203 | 203 |
| 204 def get_module(self, name, node): | 204 def get_module(self, name, node): |
| 205 """return a module by its name, looking also for relative imports; | 205 """return a module by its name, looking also for relative imports; |
| 206 raise KeyError if not found | 206 raise KeyError if not found |
| 207 """ | 207 """ |
| 208 for mod in self.modules(): | 208 for mod in self.modules(): |
| 209 mod_name = mod.node.name | 209 mod_name = mod.node.name |
| 210 if mod_name == name: | 210 if mod_name == name: |
| 211 return mod | 211 return mod |
| 212 #search for fullname of relative import modules | 212 #search for fullname of relative import modules |
| 213 package = node.root().name | 213 package = node.root().name |
| 214 if mod_name == "%s.%s" % (package, name): | 214 if mod_name == "%s.%s" % (package, name): |
| 215 return mod | 215 return mod |
| 216 if mod_name == "%s.%s" % (package.rsplit('.', 1)[0], name): | 216 if mod_name == "%s.%s" % (package.rsplit('.', 1)[0], name): |
| 217 return mod | 217 return mod |
| 218 raise KeyError(name) | 218 raise KeyError(name) |
| 219 | 219 |
| 220 def add_from_depend(self, node, from_module): | 220 def add_from_depend(self, node, from_module): |
| 221 """add dependencies created by from-imports | 221 """add dependencies created by from-imports |
| 222 """ | 222 """ |
| 223 mod_name = node.root().name | 223 mod_name = node.root().name |
| 224 obj = self.module(mod_name) | 224 obj = self.module( mod_name ) |
| 225 if from_module not in obj.node.depends: | 225 if from_module not in obj.node.depends: |
| 226 obj.node.depends.append(from_module) | 226 obj.node.depends.append(from_module) |
| 227 | 227 |
| 228 def extract_relationships(self): | 228 def extract_relationships(self): |
| 229 """extract relation ships between nodes in the diagram | 229 """extract relation ships between nodes in the diagram |
| 230 """ | 230 """ |
| 231 ClassDiagram.extract_relationships(self) | 231 ClassDiagram.extract_relationships(self) |
| 232 for obj in self.classes(): | 232 for obj in self.classes(): |
| 233 # ownership | 233 # ownership |
| 234 try: | 234 try: |
| 235 mod = self.object_from_node(obj.node.root()) | 235 mod = self.object_from_node(obj.node.root()) |
| 236 self.add_relationship(obj, mod, 'ownership') | 236 self.add_relationship(obj, mod, 'ownership') |
| 237 except KeyError: | 237 except KeyError: |
| 238 continue | 238 continue |
| 239 for obj in self.modules(): | 239 for obj in self.modules(): |
| 240 obj.shape = 'package' | 240 obj.shape = 'package' |
| 241 # dependencies | 241 # dependencies |
| 242 for dep_name in obj.node.depends: | 242 for dep_name in obj.node.depends: |
| 243 try: | 243 try: |
| 244 dep = self.get_module(dep_name, obj.node) | 244 dep = self.get_module(dep_name, obj.node) |
| 245 except KeyError: | 245 except KeyError: |
| 246 continue | 246 continue |
| 247 self.add_relationship(obj, dep, 'depends') | 247 self.add_relationship(obj, dep, 'depends') |
| OLD | NEW |