summaryrefslogtreecommitdiff
path: root/pylint/pyreverse
diff options
context:
space:
mode:
authorIonel Cristian Maries <contact@ionelmc.ro>2015-02-14 18:13:20 +0200
committerIonel Cristian Maries <contact@ionelmc.ro>2015-02-14 18:13:20 +0200
commit6d8412476a296b3a3691af1ffabcb672d9a4920f (patch)
treee358c7e886ff4d67d0efc6263f0472655efddfff /pylint/pyreverse
parent0369bd6a914af3ad92ce53eac3786bf8de785f7f (diff)
downloadpylint-6d8412476a296b3a3691af1ffabcb672d9a4920f.tar.gz
Move all package files to a pylint package.
Diffstat (limited to 'pylint/pyreverse')
-rw-r--r--pylint/pyreverse/__init__.py5
-rw-r--r--pylint/pyreverse/diadefslib.py233
-rw-r--r--pylint/pyreverse/diagrams.py247
-rw-r--r--pylint/pyreverse/main.py124
-rw-r--r--pylint/pyreverse/utils.py132
-rw-r--r--pylint/pyreverse/writer.py199
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()
+