diff options
author | root <devnull@localhost> | 2006-04-26 10:48:09 +0000 |
---|---|---|
committer | root <devnull@localhost> | 2006-04-26 10:48:09 +0000 |
commit | 8c34746eabf8ad07ebae3a3ac60aaaf353b838c9 (patch) | |
tree | e86497dd9b20b497d0056be7108072dce324c38e /pyreverse | |
download | pylint-8c34746eabf8ad07ebae3a3ac60aaaf353b838c9.tar.gz |
forget the past.
forget the past.
Diffstat (limited to 'pyreverse')
-rw-r--r-- | pyreverse/__init__.py | 5 | ||||
-rw-r--r-- | pyreverse/diadefslib.py | 286 | ||||
-rw-r--r-- | pyreverse/diagrams.py | 179 | ||||
-rw-r--r-- | pyreverse/utils.py | 192 |
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 + |