summaryrefslogtreecommitdiff
path: root/pyreverse
diff options
context:
space:
mode:
authorroot <devnull@localhost>2006-04-26 10:48:09 +0000
committerroot <devnull@localhost>2006-04-26 10:48:09 +0000
commit8c34746eabf8ad07ebae3a3ac60aaaf353b838c9 (patch)
treee86497dd9b20b497d0056be7108072dce324c38e /pyreverse
downloadpylint-8c34746eabf8ad07ebae3a3ac60aaaf353b838c9.tar.gz
forget the past.
forget the past.
Diffstat (limited to 'pyreverse')
-rw-r--r--pyreverse/__init__.py5
-rw-r--r--pyreverse/diadefslib.py286
-rw-r--r--pyreverse/diagrams.py179
-rw-r--r--pyreverse/utils.py192
4 files changed, 662 insertions, 0 deletions
diff --git a/pyreverse/__init__.py b/pyreverse/__init__.py
new file mode 100644
index 0000000..8c32ad9
--- /dev/null
+++ b/pyreverse/__init__.py
@@ -0,0 +1,5 @@
+"""
+pyreverse.extensions
+"""
+
+__revision__ = "$Id $"
diff --git a/pyreverse/diadefslib.py b/pyreverse/diadefslib.py
new file mode 100644
index 0000000..b2ad793
--- /dev/null
+++ b/pyreverse/diadefslib.py
@@ -0,0 +1,286 @@
+# Copyright (c) 2000-2004 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """
+# library to handle diagrams definition """
+
+__revision__ = "$Id: diadefslib.py,v 1.13 2006-03-14 09:56:08 syt Exp $"
+
+import sys
+
+from logilab.common.configuration import OptionsProviderMixIn
+from logilab import astng
+from logilab.astng.astng import ancestors
+from logilab.astng.utils import LocalsVisitor
+
+from pyreverse.extensions.xmlconf import DictSaxHandlerMixIn, PrefReader
+from pyreverse.extensions.diagrams import PackageDiagram, ClassDiagram
+
+
+# diadefs xml files utilities #################################################
+
+class DiaDefsSaxHandler(DictSaxHandlerMixIn):
+ """
+ definition of the structure of the diadef file, which will enable the MI
+ to fill the DiaDefs dictionary
+ """
+ _MASTER = {}
+ _S_LIST = ('class-diagram', 'package-diagram', 'state-diagram',
+ 'class', 'package')
+ _LIST = ()
+ _KEY = ('owner', 'name', 'include')
+ _CB = {}
+
+
+def read_diadefs_file(filename):
+ """read a diadef file and return the DiaDef dictionnary"""
+ diadefs = {}
+ reader = PrefReader(DiaDefsSaxHandler, diadefs)
+ try:
+ reader.fromFile(filename)
+ except:
+ import traceback
+ traceback.print_exc()
+ print >> sys.stderr, 'error while reading file %s' % filename
+ return diadefs
+
+
+class DiadefsResolverHelper:
+ """fetch objects in the project according to the diagram definition
+ from a XML file
+ """
+ def __init__(self, project, linker):
+ self._p = project
+ self.linker = linker
+
+ def resolve_packages(self, diadef, diagram=None):
+ """take a package diagram definition dictionnary and return matching
+ objects
+ """
+ if diagram is None:
+ diagram = PackageDiagram(diadef.get('name',
+ 'No name packages diagram'))
+ for package in diadef['package']:
+ name = package['name']
+ module = self.get_module(name)
+ if module is None:
+ continue
+ self.linker.visit(module)
+ diagram.add_object(name, module)
+ if package.get('include', 'no') != 'no':
+ for node, title in self.get_classes(module):
+ self.linker.visit(node)
+ diagram.add_object(title, node)
+ self.resolve_classes(diadef, diagram)
+ return diagram
+
+ def resolve_classes(self, diadef, diagram=None):
+ """take a class diagram definition dictionnary and return a Diagram
+ instance (given in paramaters or created)
+ """
+ class_defs = diadef.get('class', [])
+ if diagram is None:
+ diagram = ClassDiagram(diadef.get('name', 'No name classes diagram'))
+ for klass in class_defs:
+ name, module = klass['name'], klass.get('owner', '')
+ c = self.get_class(module, name)
+ if c is None:
+ continue
+ self.linker.visit(c)
+ diagram.add_object(name, c)
+ return diagram
+
+
+ def get_classes(self, module):
+ """return all class defined in the given astng module
+ """
+ classes = []
+ for object in module.locals.values():
+ if isinstance(object, astng.Class):
+ classes.append((object, object.name))
+ return classes
+
+ def get_module(self, name):
+ """return the astng module corresponding to name if it exists in the
+ current project
+ """
+ try:
+ return self._p.get_module(name)
+ except KeyError:
+ print >> sys.stderr, 'Warning: no module named %s' % name
+
+ def get_class(self, module, name):
+ """return the astng class corresponding to module.name if it exists in
+ the current project
+ """
+ try:
+ module = self._p.get_module(module)
+ except KeyError:
+ print >> sys.stderr, 'Warning: no module named %s' % module
+ else:
+ try:
+ return module.locals[name]
+ except KeyError:
+ print >> sys.stderr, 'Warning: no module class %s in %s' % (
+ name, module)
+
+# diagram generators ##########################################################
+
+class DefaultDiadefGenerator(LocalsVisitor):
+ """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):
+ LocalsVisitor.__init__(self)
+ self.linker = linker
+
+ def visit_project(self, node):
+ """visit an astng.Project node
+
+ create a diagram definition for packages
+ """
+ if len(node.modules) > 1:
+ self.pkgdiagram = PackageDiagram('packages %s' % node.name)
+ else:
+ self.pkgdiagram = None
+ self.classdiagram = ClassDiagram('classes %s' % node.name)
+
+ def leave_project(self, node):
+ """leave the astng.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 astng.Module node
+
+ add this class to the package diagram definition
+ """
+ if self.pkgdiagram:
+ self.linker.visit(node)
+ self.pkgdiagram.add_object(node=node, title=node.name)
+
+ def visit_class(self, node):
+ """visit an astng.Class node
+
+ add this class to the class diagram definition
+ """
+ self.linker.visit(node)
+ self.classdiagram.add_object(node=node, title=node.name)
+
+
+class ClassDiadefGenerator:
+ """generate a class diagram definition including all classes related to a
+ given class
+ """
+
+ def class_diagram(self, project, klass, linker,
+ include_level=-1, include_module_name=1):
+ """return a class diagram definition for the given klass and its related
+ klasses. Search deep depends on the include_level parameter (=1 will
+ take all classes directly related, while =2 will also take all classes
+ related to the one fecthed by=1)
+ """
+ diagram = ClassDiagram(klass)
+ if len(project.modules) > 1:
+ last_dot = klass.rfind('.')
+ module = project.get_module(klass[:last_dot])
+ klass = klass[last_dot+1:]
+ else:
+ module = project.modules[0]
+ klass = klass.split('.')[-1]
+ klass = module.resolve(klass)
+ self.extract_classes(diagram, klass, linker,
+ include_level, include_module_name)
+ return diagram
+
+ def extract_classes(self, diagram, klass_node, linker,
+ include_level, include_module_name):
+ """extract classes related to klass_node until include_level is not 0
+ """
+ if include_level == 0 or diagram.has_node(klass_node):
+ return
+ self.add_class_def(diagram, klass_node, linker, include_module_name)
+ # add all ancestors whatever the include_level ?
+ for ancestor in ancestors(klass_node):
+ self.extract_classes(diagram, ancestor, linker,
+ include_level, include_module_name)
+ include_level -= 1
+ # association
+ for name, ass_node in klass_node.instance_attrs_type.items():
+ if not isinstance(ass_node, astng.Class):
+ continue
+ self.extract_classes(diagram, ass_node, linker,
+ include_level, include_module_name)
+
+ def add_class_def(self, diagram, klass_node, linker, include_module_name):
+ """add a class definition to the class diagram
+ """
+ if include_module_name:
+ module_name = klass_node.root().name
+ title = '%s.%s' % (module_name, klass_node.name)
+ else:
+ title = klass_node.name
+ linker.visit(klass_node)
+ diagram.add_object(node=klass_node, title=title)
+
+# diagram handler #############################################################
+
+class DiadefsHandler(OptionsProviderMixIn):
+ """handle diagram definitions :
+
+ get it from user (i.e. xml files) or generate them
+ """
+
+ name = 'Diagram definition'
+ options = (
+ ("diadefs",
+ {'action':"store", 'type':'string', 'metavar': "<file>",
+ 'short' : 'd',
+ 'dest':"diadefs_file", 'default':None,
+ 'help':"create diagram according to the diagrams definitions in \
+<file>"}),
+ ("class",
+ {'action':"append", 'type':'string', 'metavar': "<class>",
+ 'dest':"classes", 'default':(),
+ 'help':"create a class diagram with all classes related to <class> "}),
+
+ )
+
+
+ def get_diadefs(self, project, linker):
+ """get the diagrams configuration data, either from a specified file or
+ generated
+ """
+ # read and interpret diagram definitions
+ diagrams = []
+ if self.config.diadefs_file is not None:
+ diadefs = read_diadefs_file(self.config.diadefs_file)
+ resolver = DiadefsResolverHelper(project, linker)
+ for class_diagram in diadefs.get('class-diagram', ()):
+ resolver.resolve_classes(class_diagram)
+ for package_diagram in diadefs.get('package-diagram', ()):
+ resolver.resolve_packages(package_diagram)
+ generator = ClassDiadefGenerator()
+ for klass in self.config.classes:
+ diagrams.append(generator.class_diagram(project, klass, linker))
+ # FIXME: generate only if no option provided
+ # or generate one
+ if not diagrams:
+ diagrams += DefaultDiadefGenerator(linker).visit(project)
+ for diagram in diagrams:
+ diagram.extract_relationships()
+ return diagrams
diff --git a/pyreverse/diagrams.py b/pyreverse/diagrams.py
new file mode 100644
index 0000000..7f281d4
--- /dev/null
+++ b/pyreverse/diagrams.py
@@ -0,0 +1,179 @@
+# Copyright (c) 2004 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.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+"""diagram objects
+"""
+
+__revision__ = "$Id: diagrams.py,v 1.6 2006-03-14 09:56:08 syt Exp $"
+
+from pyreverse.utils import is_interface
+from logilab import astng
+
+def set_counter(value):
+ Figure._UID_COUNT = value
+
+class Figure:
+ _UID_COUNT = 0
+ def __init__(self):
+ Figure._UID_COUNT += 1
+ self.fig_id = Figure._UID_COUNT
+
+class Relationship(Figure):
+ """a relation ship from an object in the diagram to another
+ """
+ def __init__(self, from_object, to_object, r_type, name=None):
+ Figure.__init__(self)
+ self.from_object = from_object
+ self.to_object = to_object
+ self.type = r_type
+ self.name = name
+
+
+class DiagramEntity(Figure):
+ """a diagram object, ie a label associated to an astng node
+ """
+ def __init__(self, title='No name', node=None):
+ Figure.__init__(self)
+ self.title = title
+ self.node = node
+
+class ClassDiagram(Figure):
+ """a class diagram objet
+ """
+ TYPE = 'class'
+ def __init__(self, title='No name'):
+ Figure.__init__(self)
+ self.title = title
+ self.objects = []
+ self.relationships = {}
+ self._nodes = {}
+
+ def add_relationship(self, from_object, to_object, r_type, name=None):
+ """create a relation ship
+ """
+ rel = Relationship(from_object, to_object, r_type, name)
+ self.relationships.setdefault(r_type, []).append(rel)
+
+ def get_relationship(self, from_object, r_type):
+ """return a relation ship or None
+ """
+ for rel in self.relationships.get(r_type, ()):
+ if rel.from_object is from_object:
+ return rel
+ raise KeyError(r_type)
+
+ def add_object(self, title, node):
+ """create a diagram object
+ """
+ assert not self._nodes.has_key(node)
+ ent = DiagramEntity(title, node)
+ self._nodes[node] = ent
+ self.objects.append(ent)
+
+ 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 self._nodes.has_key(node)
+
+ 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, astng.Class)]
+
+ def classe(self, name):
+ """return a klass 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
+ # shape
+ if is_interface(node):
+ obj.shape = 'interface'
+ else:
+ obj.shape = 'class'
+ # inheritance link
+ for par_node in node.baseobjects:
+ 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, value in node.instance_attrs_type.items():
+ try:
+ ass_obj = self.object_from_node(value)
+ self.add_relationship(obj, ass_obj, 'association', name)
+ except KeyError:
+ continue
+
+class PackageDiagram(ClassDiagram):
+ TYPE = 'package'
+
+ def modules(self):
+ """return all module nodes in the diagram"""
+ return [o for o in self.objects if isinstance(o.node, astng.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 extract_relationships(self):
+ """extract relation ships between nodes in the diagram
+ """
+ ClassDiagram.extract_relationships(self)
+ for obj in self.classes():
+ node = obj.node
+ # ownership
+ try:
+ mod = self.object_from_node(node.root())
+ self.add_relationship(obj, mod, 'ownership')
+ except KeyError:
+ continue
+ for obj in self.modules():
+ obj.shape = 'package'
+ # dependancies
+ for dep in obj.node.depends:
+ try:
+ dep = self.module(dep)
+ except KeyError:
+ continue
+ self.add_relationship(obj, dep, 'depends')
diff --git a/pyreverse/utils.py b/pyreverse/utils.py
new file mode 100644
index 0000000..def4d21
--- /dev/null
+++ b/pyreverse/utils.py
@@ -0,0 +1,192 @@
+# Copyright (c) 2002-2004 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.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+"""
+generic classes/functions for pyreverse core/extensions
+"""
+
+__revision__ = "$Id: utils.py,v 1.12 2006-03-14 09:56:06 syt Exp $"
+
+import sys
+import re
+
+from logilab.astng import ASTNGManager, IgnoreChild, Project
+from logilab.astng.utils import is_interface, is_exception, \
+ get_raises, get_returns
+from logilab.astng.manager import astng_wrapper
+
+from logilab.common.configuration import ConfigurationMixIn
+
+from pyreverse.__pkginfo__ import version
+from pyreverse import config
+
+def time_tag():
+ """
+ return a timestamp as string
+ """
+ from time import time, localtime, strftime
+ return strftime('%b %d at %T', localtime(time()))
+
+def LOG(msg):
+ """LOG doesn't do anything by default"""
+ pass
+
+def info(msg):
+ """print an informal message on stdout"""
+ LOG(msg)
+
+
+# astng utilities #############################################################
+
+PROTECTED = re.compile('^_[A-Za-z]+(_*[A-Za-z]+)*_?$')
+PRIVATE = re.compile('^__[A-Za-z]+(_*[A-Za-z]+)*_?$')
+SPECIAL = re.compile('^__[A-Za-z]+__$')
+
+def get_visibility(name):
+ """return the visibility from a name: public, protected, private or special
+ """
+ if SPECIAL.match(name):
+ visibility = 'special'
+ elif PROTECTED.match(name):
+ visibility = 'protected'
+ elif PRIVATE.match(name):
+ visibility = 'private'
+ 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)
+
+
+
+# Helpers #####################################################################
+
+_CONSTRUCTOR = 1
+_SPECIAL = 2
+_PROTECTED = 4
+_PRIVATE = 8
+MODES = {
+ 'ALL' : 0,
+ 'PUB_ONLY' : _SPECIAL + _PROTECTED + _PRIVATE,
+ 'SPECIAL' : _SPECIAL,
+ 'OTHER' : _PROTECTED + _PRIVATE,
+}
+
+class FilterMixIn:
+ """filter nodes according to a mode and nodes'visibility
+ """
+
+
+ options = (
+ ("filter-mode",
+ {'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"""}),
+ )
+
+ def __init__(self):
+ pass
+
+ def get_mode(self):
+ """return the integer value of a mode string
+ """
+ try:
+ return self.__mode
+ except AttributeError:
+ mode = 0
+ for nummod in self.config.mode.split('+'):
+ try:
+ mode += MODES[nummod]
+ except KeyError, ex:
+ print >> sys.stderr, 'Unknown filter mode %s' % ex
+ self.__mode = mode
+ return mode
+
+ def filter(self, node):
+ """return true if the node should be treaten
+ """
+ mode = self.get_mode()
+ visibility = get_visibility(getattr(node, 'name', node))
+ if mode & _SPECIAL and visibility == 'special':
+ return 0
+ if mode & _PROTECTED and visibility == 'protected':
+ return 0
+ if mode & _PRIVATE and visibility == 'private':
+ return 0
+ return 1
+
+
+class RunHelper(ConfigurationMixIn):
+ """command line helper
+ """
+ name = 'main'
+ options = (('quiet', {'help' : 'run quietly', 'action' : 'store_true',
+ 'short': 'q'}),
+ )
+
+ def __init__(self, usage, options_providers):
+ ConfigurationMixIn.__init__(self, """\
+USAGE: %%prog [options] <file or module>...
+%s""" % usage, version="%%prog %s" % version)
+ config.insert_default_options()
+ manager = ASTNGManager()
+ # FIXME: use an infinite cache
+ manager._cache = {}
+ # add options
+ self.register_options_provider(manager)
+ for provider in options_providers:
+ self.register_options_provider(provider)
+ #self.load_file_configuration()
+ args = self.load_command_line_configuration()
+ if not args:
+ self.help()
+ else:
+ global LOG
+ LOG = self.log
+ # extract project representation
+ project = manager.project_from_files(args, astng_wrapper)
+ self.do_run(project)
+
+ def do_run(self, project):
+ """method to override in concrete classes"""
+ raise NotImplementedError()
+
+ def log(self, msg):
+ """print an informal message on stdout"""
+ if not self.config.quiet:
+ print '-'*80
+ print msg
+