diff options
author | Ionel Cristian Maries <contact@ionelmc.ro> | 2015-02-14 18:13:20 +0200 |
---|---|---|
committer | Ionel Cristian Maries <contact@ionelmc.ro> | 2015-02-14 18:13:20 +0200 |
commit | 6d8412476a296b3a3691af1ffabcb672d9a4920f (patch) | |
tree | e358c7e886ff4d67d0efc6263f0472655efddfff /pylint/pyreverse | |
parent | 0369bd6a914af3ad92ce53eac3786bf8de785f7f (diff) | |
download | pylint-6d8412476a296b3a3691af1ffabcb672d9a4920f.tar.gz |
Move all package files to a pylint package.
Diffstat (limited to 'pylint/pyreverse')
-rw-r--r-- | pylint/pyreverse/__init__.py | 5 | ||||
-rw-r--r-- | pylint/pyreverse/diadefslib.py | 233 | ||||
-rw-r--r-- | pylint/pyreverse/diagrams.py | 247 | ||||
-rw-r--r-- | pylint/pyreverse/main.py | 124 | ||||
-rw-r--r-- | pylint/pyreverse/utils.py | 132 | ||||
-rw-r--r-- | pylint/pyreverse/writer.py | 199 |
6 files changed, 940 insertions, 0 deletions
diff --git a/pylint/pyreverse/__init__.py b/pylint/pyreverse/__init__.py new file mode 100644 index 0000000..8c32ad9 --- /dev/null +++ b/pylint/pyreverse/__init__.py @@ -0,0 +1,5 @@ +""" +pyreverse.extensions +""" + +__revision__ = "$Id $" diff --git a/pylint/pyreverse/diadefslib.py b/pylint/pyreverse/diadefslib.py new file mode 100644 index 0000000..e0dc8cf --- /dev/null +++ b/pylint/pyreverse/diadefslib.py @@ -0,0 +1,233 @@ +# Copyright (c) 2000-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""handle diagram generation options for class diagram or default diagrams +""" + +from logilab.common.compat import builtins + +import astroid +from astroid.utils import LocalsVisitor + +from pylint.pyreverse.diagrams import PackageDiagram, ClassDiagram + +BUILTINS_NAME = builtins.__name__ + +# diagram generators ########################################################## + +class DiaDefGenerator(object): + """handle diagram generation options""" + + def __init__(self, linker, handler): + """common Diagram Handler initialization""" + self.config = handler.config + self._set_default_options() + self.linker = linker + self.classdiagram = None # defined by subclasses + + def get_title(self, node): + """get title for objects""" + title = node.name + if self.module_names: + title = '%s.%s' % (node.root().name, title) + return title + + def _set_option(self, option): + """activate some options if not explicitly deactivated""" + # if we have a class diagram, we want more information by default; + # so if the option is None, we return True + if option is None: + if self.config.classes: + return True + else: + return False + return option + + def _set_default_options(self): + """set different default options with _default dictionary""" + self.module_names = self._set_option(self.config.module_names) + all_ancestors = self._set_option(self.config.all_ancestors) + all_associated = self._set_option(self.config.all_associated) + anc_level, ass_level = (0, 0) + if all_ancestors: + anc_level = -1 + if all_associated: + ass_level = -1 + if self.config.show_ancestors is not None: + anc_level = self.config.show_ancestors + if self.config.show_associated is not None: + ass_level = self.config.show_associated + self.anc_level, self.ass_level = anc_level, ass_level + + def _get_levels(self): + """help function for search levels""" + return self.anc_level, self.ass_level + + def show_node(self, node): + """true if builtins and not show_builtins""" + if self.config.show_builtin: + return True + return node.root().name != BUILTINS_NAME + + def add_class(self, node): + """visit one class and add it to diagram""" + self.linker.visit(node) + self.classdiagram.add_object(self.get_title(node), node) + + def get_ancestors(self, node, level): + """return ancestor nodes of a class node""" + if level == 0: + return + for ancestor in node.ancestors(recurs=False): + if not self.show_node(ancestor): + continue + yield ancestor + + def get_associated(self, klass_node, level): + """return associated nodes of a class node""" + if level == 0: + return + for ass_nodes in list(klass_node.instance_attrs_type.values()) + \ + list(klass_node.locals_type.values()): + for ass_node in ass_nodes: + if isinstance(ass_node, astroid.Instance): + ass_node = ass_node._proxied + if not (isinstance(ass_node, astroid.Class) + and self.show_node(ass_node)): + continue + yield ass_node + + def extract_classes(self, klass_node, anc_level, ass_level): + """extract recursively classes related to klass_node""" + if self.classdiagram.has_node(klass_node) or not self.show_node(klass_node): + return + self.add_class(klass_node) + + for ancestor in self.get_ancestors(klass_node, anc_level): + self.extract_classes(ancestor, anc_level-1, ass_level) + + for ass_node in self.get_associated(klass_node, ass_level): + self.extract_classes(ass_node, anc_level, ass_level-1) + + +class DefaultDiadefGenerator(LocalsVisitor, DiaDefGenerator): + """generate minimum diagram definition for the project : + + * a package diagram including project's modules + * a class diagram including project's classes + """ + + def __init__(self, linker, handler): + DiaDefGenerator.__init__(self, linker, handler) + LocalsVisitor.__init__(self) + + def visit_project(self, node): + """visit an astroid.Project node + + create a diagram definition for packages + """ + mode = self.config.mode + if len(node.modules) > 1: + self.pkgdiagram = PackageDiagram('packages %s' % node.name, mode) + else: + self.pkgdiagram = None + self.classdiagram = ClassDiagram('classes %s' % node.name, mode) + + def leave_project(self, node): # pylint: disable=unused-argument + """leave the astroid.Project node + + return the generated diagram definition + """ + if self.pkgdiagram: + return self.pkgdiagram, self.classdiagram + return self.classdiagram, + + def visit_module(self, node): + """visit an astroid.Module node + + add this class to the package diagram definition + """ + if self.pkgdiagram: + self.linker.visit(node) + self.pkgdiagram.add_object(node.name, node) + + def visit_class(self, node): + """visit an astroid.Class node + + add this class to the class diagram definition + """ + anc_level, ass_level = self._get_levels() + self.extract_classes(node, anc_level, ass_level) + + def visit_from(self, node): + """visit astroid.From and catch modules for package diagram + """ + if self.pkgdiagram: + self.pkgdiagram.add_from_depend(node, node.modname) + + +class ClassDiadefGenerator(DiaDefGenerator): + """generate a class diagram definition including all classes related to a + given class + """ + + def __init__(self, linker, handler): + DiaDefGenerator.__init__(self, linker, handler) + + def class_diagram(self, project, klass): + """return a class diagram definition for the given klass and its + related klasses + """ + + self.classdiagram = ClassDiagram(klass, self.config.mode) + if len(project.modules) > 1: + module, klass = klass.rsplit('.', 1) + module = project.get_module(module) + else: + module = project.modules[0] + klass = klass.split('.')[-1] + klass = next(module.ilookup(klass)) + + anc_level, ass_level = self._get_levels() + self.extract_classes(klass, anc_level, ass_level) + return self.classdiagram + +# diagram handler ############################################################# + +class DiadefsHandler(object): + """handle diagram definitions : + + get it from user (i.e. xml files) or generate them + """ + + def __init__(self, config): + self.config = config + + def get_diadefs(self, project, linker): + """get the diagrams configuration data + :param linker: astroid.inspector.Linker(IdGeneratorMixIn, LocalsVisitor) + :param project: astroid.manager.Project + """ + + # read and interpret diagram definitions (Diadefs) + diagrams = [] + generator = ClassDiadefGenerator(linker, self) + for klass in self.config.classes: + diagrams.append(generator.class_diagram(project, klass)) + if not diagrams: + diagrams = DefaultDiadefGenerator(linker, self).visit(project) + for diagram in diagrams: + diagram.extract_relationships() + return diagrams diff --git a/pylint/pyreverse/diagrams.py b/pylint/pyreverse/diagrams.py new file mode 100644 index 0000000..f0d7a92 --- /dev/null +++ b/pylint/pyreverse/diagrams.py @@ -0,0 +1,247 @@ +# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""diagram objects +""" + +import astroid +from pylint.pyreverse.utils import is_interface, FilterMixIn + +class Figure(object): + """base class for counter handling""" + +class Relationship(Figure): + """a relation ship from an object in the diagram to another + """ + def __init__(self, from_object, to_object, relation_type, name=None): + Figure.__init__(self) + self.from_object = from_object + self.to_object = to_object + self.type = relation_type + self.name = name + + +class DiagramEntity(Figure): + """a diagram object, i.e. a label associated to an astroid node + """ + def __init__(self, title='No name', node=None): + Figure.__init__(self) + self.title = title + self.node = node + +class ClassDiagram(Figure, FilterMixIn): + """main class diagram handling + """ + TYPE = 'class' + def __init__(self, title, mode): + FilterMixIn.__init__(self, mode) + Figure.__init__(self) + self.title = title + self.objects = [] + self.relationships = {} + self._nodes = {} + self.depends = [] + + def get_relationships(self, role): + # sorted to get predictable (hence testable) results + return sorted(self.relationships.get(role, ()), + key=lambda x: (x.from_object.fig_id, x.to_object.fig_id)) + + def add_relationship(self, from_object, to_object, + relation_type, name=None): + """create a relation ship + """ + rel = Relationship(from_object, to_object, relation_type, name) + self.relationships.setdefault(relation_type, []).append(rel) + + def get_relationship(self, from_object, relation_type): + """return a relation ship or None + """ + for rel in self.relationships.get(relation_type, ()): + if rel.from_object is from_object: + return rel + raise KeyError(relation_type) + + def get_attrs(self, node): + """return visible attributes, possibly with class name""" + attrs = [] + for node_name, ass_nodes in list(node.instance_attrs_type.items()) + \ + list(node.locals_type.items()): + if not self.show_attr(node_name): + continue + names = self.class_names(ass_nodes) + if names: + node_name = "%s : %s" % (node_name, ", ".join(names)) + attrs.append(node_name) + return sorted(attrs) + + def get_methods(self, node): + """return visible methods""" + methods = [ + m for m in node.values() + if isinstance(m, astroid.Function) and self.show_attr(m.name) + ] + return sorted(methods, key=lambda n: n.name) + + def add_object(self, title, node): + """create a diagram object + """ + assert node not in self._nodes + ent = DiagramEntity(title, node) + self._nodes[node] = ent + self.objects.append(ent) + + def class_names(self, nodes): + """return class names if needed in diagram""" + names = [] + for ass_node in nodes: + if isinstance(ass_node, astroid.Instance): + ass_node = ass_node._proxied + if isinstance(ass_node, astroid.Class) \ + and hasattr(ass_node, "name") and not self.has_node(ass_node): + if ass_node.name not in names: + ass_name = ass_node.name + names.append(ass_name) + return names + + def nodes(self): + """return the list of underlying nodes + """ + return self._nodes.keys() + + def has_node(self, node): + """return true if the given node is included in the diagram + """ + return node in self._nodes + + def object_from_node(self, node): + """return the diagram object mapped to node + """ + return self._nodes[node] + + def classes(self): + """return all class nodes in the diagram""" + return [o for o in self.objects if isinstance(o.node, astroid.Class)] + + def classe(self, name): + """return a class by its name, raise KeyError if not found + """ + for klass in self.classes(): + if klass.node.name == name: + return klass + raise KeyError(name) + + def extract_relationships(self): + """extract relation ships between nodes in the diagram + """ + for obj in self.classes(): + node = obj.node + obj.attrs = self.get_attrs(node) + obj.methods = self.get_methods(node) + # shape + if is_interface(node): + obj.shape = 'interface' + else: + obj.shape = 'class' + # inheritance link + for par_node in node.ancestors(recurs=False): + try: + par_obj = self.object_from_node(par_node) + self.add_relationship(obj, par_obj, 'specialization') + except KeyError: + continue + # implements link + for impl_node in node.implements: + try: + impl_obj = self.object_from_node(impl_node) + self.add_relationship(obj, impl_obj, 'implements') + except KeyError: + continue + # associations link + for name, values in list(node.instance_attrs_type.items()) + \ + list(node.locals_type.items()): + for value in values: + if value is astroid.YES: + continue + if isinstance(value, astroid.Instance): + value = value._proxied + try: + ass_obj = self.object_from_node(value) + self.add_relationship(ass_obj, obj, 'association', name) + except KeyError: + continue + + +class PackageDiagram(ClassDiagram): + """package diagram handling + """ + TYPE = 'package' + + def modules(self): + """return all module nodes in the diagram""" + return [o for o in self.objects if isinstance(o.node, astroid.Module)] + + def module(self, name): + """return a module by its name, raise KeyError if not found + """ + for mod in self.modules(): + if mod.node.name == name: + return mod + raise KeyError(name) + + def get_module(self, name, node): + """return a module by its name, looking also for relative imports; + raise KeyError if not found + """ + for mod in self.modules(): + mod_name = mod.node.name + if mod_name == name: + return mod + #search for fullname of relative import modules + package = node.root().name + if mod_name == "%s.%s" % (package, name): + return mod + if mod_name == "%s.%s" % (package.rsplit('.', 1)[0], name): + return mod + raise KeyError(name) + + def add_from_depend(self, node, from_module): + """add dependencies created by from-imports + """ + mod_name = node.root().name + obj = self.module(mod_name) + if from_module not in obj.node.depends: + obj.node.depends.append(from_module) + + def extract_relationships(self): + """extract relation ships between nodes in the diagram + """ + ClassDiagram.extract_relationships(self) + for obj in self.classes(): + # ownership + try: + mod = self.object_from_node(obj.node.root()) + self.add_relationship(obj, mod, 'ownership') + except KeyError: + continue + for obj in self.modules(): + obj.shape = 'package' + # dependencies + for dep_name in obj.node.depends: + try: + dep = self.get_module(dep_name, obj.node) + except KeyError: + continue + self.add_relationship(obj, dep, 'depends') diff --git a/pylint/pyreverse/main.py b/pylint/pyreverse/main.py new file mode 100644 index 0000000..408c172 --- /dev/null +++ b/pylint/pyreverse/main.py @@ -0,0 +1,124 @@ +# # Copyright (c) 2000-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" + %prog [options] <packages> + + create UML diagrams for classes and modules in <packages> +""" +from __future__ import print_function + +import sys, os +from logilab.common.configuration import ConfigurationMixIn +from astroid.manager import AstroidManager +from astroid.inspector import Linker + +from pylint.pyreverse.diadefslib import DiadefsHandler +from pylint.pyreverse import writer +from pylint.pyreverse.utils import insert_default_options + +OPTIONS = ( + ("filter-mode", + dict(short='f', default='PUB_ONLY', dest='mode', type='string', + action='store', metavar='<mode>', + help="""filter attributes and functions according to + <mode>. Correct modes are : + 'PUB_ONLY' filter all non public attributes + [DEFAULT], equivalent to PRIVATE+SPECIAL_A + 'ALL' no filter + 'SPECIAL' filter Python special functions + except constructor + 'OTHER' filter protected and private + attributes""")), + + ("class", + dict(short='c', action="append", metavar="<class>", dest="classes", default=[], + help="create a class diagram with all classes related to <class>;\ + this uses by default the options -ASmy")), + + ("show-ancestors", + dict(short="a", action="store", metavar='<ancestor>', type='int', + help='show <ancestor> generations of ancestor classes not in <projects>')), + ("all-ancestors", + dict(short="A", default=None, + help="show all ancestors off all classes in <projects>")), + ("show-associated", + dict(short='s', action="store", metavar='<ass_level>', type='int', + help='show <ass_level> levels of associated classes not in <projects>')), + ("all-associated", + dict(short='S', default=None, + help='show recursively all associated off all associated classes')), + ("show-builtin", + dict(short="b", action="store_true", default=False, + help='include builtin objects in representation of classes')), + + ("module-names", + dict(short="m", default=None, type='yn', metavar='[yn]', + help='include module name in representation of classes')), + # TODO : generate dependencies like in pylint + # ("package-dependencies", + # dict(short="M", action="store", metavar='<package_depth>', type='int', + # help='show <package_depth> module dependencies beyond modules in \ + # <projects> (for the package diagram)')), + ("only-classnames", + dict(short='k', action="store_true", default=False, + help="don't show attributes and methods in the class boxes; \ +this disables -f values")), + ("output", dict(short="o", dest="output_format", action="store", + default="dot", metavar="<format>", + help="create a *.<format> output file if format available.")), +) +# FIXME : quiet mode +#( ('quiet', + #dict(help='run quietly', action='store_true', short='q')), ) + +class Run(ConfigurationMixIn): + """base class providing common behaviour for pyreverse commands""" + + options = OPTIONS + + def __init__(self, args): + ConfigurationMixIn.__init__(self, usage=__doc__) + insert_default_options() + self.manager = AstroidManager() + self.register_options_provider(self.manager) + args = self.load_command_line_configuration() + sys.exit(self.run(args)) + + def run(self, args): + """checking arguments and run project""" + if not args: + print(self.help()) + return 1 + # insert current working directory to the python path to recognize + # dependencies to local modules even if cwd is not in the PYTHONPATH + sys.path.insert(0, os.getcwd()) + try: + project = self.manager.project_from_files(args) + linker = Linker(project, tag=True) + handler = DiadefsHandler(self.config) + diadefs = handler.get_diadefs(project, linker) + finally: + sys.path.pop(0) + + if self.config.output_format == "vcg": + writer.VCGWriter(self.config).write(diadefs) + else: + writer.DotWriter(self.config).write(diadefs) + return 0 + + +if __name__ == '__main__': + Run(sys.argv[1:]) diff --git a/pylint/pyreverse/utils.py b/pylint/pyreverse/utils.py new file mode 100644 index 0000000..5d6d133 --- /dev/null +++ b/pylint/pyreverse/utils.py @@ -0,0 +1,132 @@ +# Copyright (c) 2002-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +""" +generic classes/functions for pyreverse core/extensions +""" +from __future__ import print_function + +import sys +import re +import os + +########### pyreverse option utils ############################## + + +RCFILE = '.pyreverserc' + +def get_default_options(): + """ + Read config file and return list of options + """ + options = [] + home = os.environ.get('HOME', '') + if home: + rcfile = os.path.join(home, RCFILE) + try: + options = open(rcfile).read().split() + except IOError: + pass # ignore if no config file found + return options + +def insert_default_options(): + """insert default options to sys.argv + """ + options = get_default_options() + options.reverse() + for arg in options: + sys.argv.insert(1, arg) + + + +# astroid utilities ########################################################### + +SPECIAL = re.compile('^__[A-Za-z0-9]+[A-Za-z0-9_]*__$') +PRIVATE = re.compile('^__[_A-Za-z0-9]*[A-Za-z0-9]+_?$') +PROTECTED = re.compile('^_[_A-Za-z0-9]*$') + +def get_visibility(name): + """return the visibility from a name: public, protected, private or special + """ + if SPECIAL.match(name): + visibility = 'special' + elif PRIVATE.match(name): + visibility = 'private' + elif PROTECTED.match(name): + visibility = 'protected' + + else: + visibility = 'public' + return visibility + +ABSTRACT = re.compile('^.*Abstract.*') +FINAL = re.compile('^[A-Z_]*$') + +def is_abstract(node): + """return true if the given class node correspond to an abstract class + definition + """ + return ABSTRACT.match(node.name) + +def is_final(node): + """return true if the given class/function node correspond to final + definition + """ + return FINAL.match(node.name) + +def is_interface(node): + # bw compat + return node.type == 'interface' + +def is_exception(node): + # bw compat + return node.type == 'exception' + + +# Helpers ##################################################################### + +_CONSTRUCTOR = 1 +_SPECIAL = 2 +_PROTECTED = 4 +_PRIVATE = 8 +MODES = { + 'ALL' : 0, + 'PUB_ONLY' : _SPECIAL + _PROTECTED + _PRIVATE, + 'SPECIAL' : _SPECIAL, + 'OTHER' : _PROTECTED + _PRIVATE, +} +VIS_MOD = {'special': _SPECIAL, 'protected': _PROTECTED, + 'private': _PRIVATE, 'public': 0} + +class FilterMixIn(object): + """filter nodes according to a mode and nodes' visibility + """ + def __init__(self, mode): + "init filter modes" + __mode = 0 + for nummod in mode.split('+'): + try: + __mode += MODES[nummod] + except KeyError as ex: + print('Unknown filter mode %s' % ex, file=sys.stderr) + self.__mode = __mode + + + def show_attr(self, node): + """return true if the node should be treated + """ + visibility = get_visibility(getattr(node, 'name', node)) + return not self.__mode & VIS_MOD[visibility] + diff --git a/pylint/pyreverse/writer.py b/pylint/pyreverse/writer.py new file mode 100644 index 0000000..8628a8c --- /dev/null +++ b/pylint/pyreverse/writer.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2008-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""Utilities for creating VCG and Dot diagrams""" + +from logilab.common.vcgutils import VCGPrinter +from logilab.common.graph import DotBackend + +from pylint.pyreverse.utils import is_exception + +class DiagramWriter(object): + """base class for writing project diagrams + """ + def __init__(self, config, styles): + self.config = config + self.pkg_edges, self.inh_edges, self.imp_edges, self.ass_edges = styles + self.printer = None # defined in set_printer + + def write(self, diadefs): + """write files for <project> according to <diadefs> + """ + for diagram in diadefs: + basename = diagram.title.strip().replace(' ', '_') + file_name = '%s.%s' % (basename, self.config.output_format) + self.set_printer(file_name, basename) + if diagram.TYPE == 'class': + self.write_classes(diagram) + else: + self.write_packages(diagram) + self.close_graph() + + def write_packages(self, diagram): + """write a package diagram""" + # sorted to get predictable (hence testable) results + for i, obj in enumerate(sorted(diagram.modules(), key=lambda x: x.title)): + self.printer.emit_node(i, label=self.get_title(obj), shape='box') + obj.fig_id = i + # package dependencies + for rel in diagram.get_relationships('depends'): + self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id, + **self.pkg_edges) + + def write_classes(self, diagram): + """write a class diagram""" + # sorted to get predictable (hence testable) results + for i, obj in enumerate(sorted(diagram.objects, key=lambda x: x.title)): + self.printer.emit_node(i, **self.get_values(obj)) + obj.fig_id = i + # inheritance links + for rel in diagram.get_relationships('specialization'): + self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id, + **self.inh_edges) + # implementation links + for rel in diagram.get_relationships('implements'): + self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id, + **self.imp_edges) + # generate associations + for rel in diagram.get_relationships('association'): + self.printer.emit_edge(rel.from_object.fig_id, rel.to_object.fig_id, + label=rel.name, **self.ass_edges) + + def set_printer(self, file_name, basename): + """set printer""" + raise NotImplementedError + + def get_title(self, obj): + """get project title""" + raise NotImplementedError + + def get_values(self, obj): + """get label and shape for classes.""" + raise NotImplementedError + + def close_graph(self): + """finalize the graph""" + raise NotImplementedError + + +class DotWriter(DiagramWriter): + """write dot graphs from a diagram definition and a project + """ + + def __init__(self, config): + styles = [dict(arrowtail='none', arrowhead="open"), + dict(arrowtail='none', arrowhead='empty'), + dict(arrowtail='node', arrowhead='empty', style='dashed'), + dict(fontcolor='green', arrowtail='none', + arrowhead='diamond', style='solid'), + ] + DiagramWriter.__init__(self, config, styles) + + def set_printer(self, file_name, basename): + """initialize DotWriter and add options for layout. + """ + layout = dict(rankdir="BT") + self.printer = DotBackend(basename, additionnal_param=layout) + self.file_name = file_name + + def get_title(self, obj): + """get project title""" + return obj.title + + def get_values(self, obj): + """get label and shape for classes. + + The label contains all attributes and methods + """ + label = obj.title + if obj.shape == 'interface': + label = u'«interface»\\n%s' % label + if not self.config.only_classnames: + label = r'%s|%s\l|' % (label, r'\l'.join(obj.attrs)) + for func in obj.methods: + label = r'%s%s()\l' % (label, func.name) + label = '{%s}' % label + if is_exception(obj.node): + return dict(fontcolor='red', label=label, shape='record') + return dict(label=label, shape='record') + + def close_graph(self): + """print the dot graph into <file_name>""" + self.printer.generate(self.file_name) + + +class VCGWriter(DiagramWriter): + """write vcg graphs from a diagram definition and a project + """ + def __init__(self, config): + styles = [dict(arrowstyle='solid', backarrowstyle='none', + backarrowsize=0), + dict(arrowstyle='solid', backarrowstyle='none', + backarrowsize=10), + dict(arrowstyle='solid', backarrowstyle='none', + linestyle='dotted', backarrowsize=10), + dict(arrowstyle='solid', backarrowstyle='none', + textcolor='green'), + ] + DiagramWriter.__init__(self, config, styles) + + def set_printer(self, file_name, basename): + """initialize VCGWriter for a UML graph""" + self.graph_file = open(file_name, 'w+') + self.printer = VCGPrinter(self.graph_file) + self.printer.open_graph(title=basename, layoutalgorithm='dfs', + late_edge_labels='yes', port_sharing='no', + manhattan_edges='yes') + self.printer.emit_node = self.printer.node + self.printer.emit_edge = self.printer.edge + + def get_title(self, obj): + """get project title in vcg format""" + return r'\fb%s\fn' % obj.title + + def get_values(self, obj): + """get label and shape for classes. + + The label contains all attributes and methods + """ + if is_exception(obj.node): + label = r'\fb\f09%s\fn' % obj.title + else: + label = r'\fb%s\fn' % obj.title + if obj.shape == 'interface': + shape = 'ellipse' + else: + shape = 'box' + if not self.config.only_classnames: + attrs = obj.attrs + methods = [func.name for func in obj.methods] + # box width for UML like diagram + maxlen = max(len(name) for name in [obj.title] + methods + attrs) + line = '_' * (maxlen + 2) + label = r'%s\n\f%s' % (label, line) + for attr in attrs: + label = r'%s\n\f08%s' % (label, attr) + if attrs: + label = r'%s\n\f%s' % (label, line) + for func in methods: + label = r'%s\n\f10%s()' % (label, func) + return dict(label=label, shape=shape) + + def close_graph(self): + """close graph and file""" + self.printer.close_graph() + self.graph_file.close() + |