diff options
Diffstat (limited to 'umlpy.py')
-rw-r--r-- | umlpy.py | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/umlpy.py b/umlpy.py new file mode 100644 index 0000000..0a3cd48 --- /dev/null +++ b/umlpy.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python +# coding=utf-8 + +''' + umlpy + Author : Guillaume "iXce" Seguin + Email : guillaume@segu.in (or guillaume.seguin@ens.fr) + + # UML-like graph produced for Python # + + Copyright (C) 2009 Guillaume Seguin + + 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. +''' + +import epydoc.docparser +import epydoc.docstringparser +import epydoc.apidoc + +import gv + +import sys +import os +import re + +from optparse import OptionParser + +parser = OptionParser() +parser.add_option("-v", dest = "debug", action = "store_true", + default = False, + help = "enables debug output") +parser.add_option("-e", "--exclude", dest = "excludes", action = "append", + default = [], + help = "classes matching this regexp will be excluded \ + from display") +parser.add_option("-i", "--include", dest = "includes", action = "append", + default = [], + help = "classes matching this regexp will be included \ + in display even if they were excluded by exclude \ + regexps") +parser.add_option("-p", "--prefix", dest = "prefix", action = "store", + default = "", + help = "prefix which will be stripped of class names") +parser.add_option("-f", "--force", dest = "forces", action = "append", + default = [], + help = "classes matching this regexp will be forced into \ + display") +parser.add_option("--all-methods", dest = "all_methods", + action = "store_true", default = False, + help = "shows all methods") +parser.add_option("--all-properties", dest = "all_properties", + action = "store_true", default = False, + help = "shows all properties") +parser.add_option("--no-method", dest = "no_method", + action = "store_true", default = False, + help = "shows no method") +parser.add_option("--no-property", dest = "no_property", + action = "store_true", default = False, + help = "shows no property") +parser.add_option("-o", "--output", dest = "output", action = "store", + default = "uml.png", + help = "output file") + +(options, args) = parser.parse_args () + +excludes = reduce (lambda x, y: x + y, + map (lambda s: s.split(","), options.excludes), []) +includes = reduce (lambda x, y: x + y, + map (lambda s: s.split(","), options.includes), []) +forces = reduce (lambda x, y: x + y, + map (lambda s: s.split(","), options.forces), []) +prefix = options.prefix +all_methods = options.all_methods +all_properties = options.all_properties +no_method = options.no_method +no_property = options.no_property +output = options.output + +print "Settings" +print "--------" +print "Output file :", output +print "Prefix :", prefix +print "Show all methods :", all_methods +print "Show all properties :", all_properties +print "Show no method :", no_method +print "Show no propertie :", no_property +print "Excludes :", excludes +print "Includes :", includes +print "Forces :", forces + +excludes = [re.compile (exp) + for exp in excludes] +includes = [re.compile (exp) + for exp in includes] +forces = [re.compile (exp) + for exp in forces] + +def is_excluded (class_name): + for exp in excludes: + if exp.match (class_name): + for exp in includes: + if exp.match (class_name): + return False + for exp in forces: + if exp.match (class_name): + return False + return True + else: + return False + +docs = [] + +def get_var_type (var): + if var.type_descr != None: + return str (var.type_descr.to_plaintext ("").strip ()) + else: + return None + +for path in args: + if os.path.isdir (path): + paths = [os.path.join (path, file) + for file in os.listdir (path) if file.endswith (".py")] + else: + paths = [path] + for path in paths: + docs.append (epydoc.docparser.parse_docs (path)) + +classes = [] +bases_dict = {} +uses_dict = {} +methods_dict = {} +vars_dict = {} + +for doc in docs: + for var in doc.variables.values (): + if type (var.value) != epydoc.apidoc.ClassDoc: + continue + var_val = var.value + var_name = str (var_val.canonical_name) + var_name = var_name.replace (prefix, "") + classes.append (var_name) + if options.debug: + print var_name + var_vars = var_val.variables.values () + vars_dict[var_name] = [] + methods_dict[var_name] = [] + uses_dict[var_name] = [] + if str (var_val.docstring) != "<UNKNOWN>": + bits = str (var_val.docstring).split ("\n") + for bit in bits: + if bit.strip ().startswith ("@uses:"): + bit = bit.replace ("@uses:", "").strip () + uses_dict[var_name].append (bit) + for var_var in var_vars: + if type (var_var.value) == epydoc.apidoc.GenericValueDoc: + if no_property: + continue + epydoc.docstringparser.parse_docstring (var_var, None) + if options.debug: + print var_name, var_var.name, get_var_type (var_var) + var_type = get_var_type (var_var) + if all_properties or "@doc" in str (var_var.docstring) or var_type: + var_var_name = str (var_var.name.replace (prefix, "")) + vars_dict[var_name].append ((var_var_name, get_var_type (var_var))) + elif type (var_var.value) in (epydoc.apidoc.RoutineDoc, + epydoc.apidoc.StaticMethodDoc): + if no_method: + continue + if (all_methods or "@doc" in str (var_var.value.docstring)) \ + and not "@nodoc" in str (var_var.value.docstring): + methods_dict[var_name].append (str (var_var.name)) + else: + print type (var_var.value) + bases = [str (base.canonical_name).replace (prefix, "") + for base in var_val.bases + if str (base.canonical_name) not in ("object", "<UNKNOWN>")] + if options.debug: + print bases + bases_dict[var_name] = bases + +nodes_dict = {} +var_fields_dict = {} +method_fields_dict = {} +graph = gv.digraph ('g') +gv.setv (graph, 'charset', 'utf-8') +gv.setv (graph, 'overlap', 'false') +gv.setv (graph, 'splines', 'true') +gv.setv (graph, 'rankdir', 'BT') +item = gv.protoedge (graph) +gv.setv (item, 'len', '2') +item = gv.protonode (graph) +gv.setv (item, 'shape', 'plaintext') + +CLASS_COLOR = "#FF6262" +VAR_FIELD_COLOR = "#63BDFF" +METHOD_FIELD_COLOR = "#6EFF62" + +SUPER_EDGE_COLOR = "#AD0006" +VAR_EDGE_COLOR = "#002990" +USE_EDGE_COLOR = "#05C800" + +LABEL_BASE = """< +<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> + <TR> + <TD BGCOLOR="%s" PORT="%s">%s</TD> + </TR> +%s +</TABLE> +>""" +FIELD_FORMATTER = """ <TR> + <TD BGCOLOR="%s" PORT="%s">%s</TD> + </TR>""" + +def build_record_label (class_name): + fields = "".join ([FIELD_FORMATTER % (VAR_FIELD_COLOR, field, field) + for field in sorted (var_fields_dict[class_name])]) + fields += "".join ([FIELD_FORMATTER % (METHOD_FIELD_COLOR, field, field) + for field in sorted (method_fields_dict[class_name])]) + return LABEL_BASE % (CLASS_COLOR, class_name, class_name, fields) + +def build_simple_label (class_name): + return LABEL_BASE % (CLASS_COLOR, class_name, class_name, "") + +def check_class_node (name): + if name not in nodes_dict: + node = gv.node (graph, name) + gv.setv (node, 'shape', 'plaintext') + gv.setv (node, 'style', 'invis') + if name in var_fields_dict or name in method_fields_dict: + gv.setv (node, 'label', build_record_label (name)) + else: + gv.setv (node, 'label', build_simple_label (name)) + nodes_dict[name] = node + return nodes_dict[name] + +def add_super_edge (class_name, base_name): + check_class_node (class_name) + check_class_node (base_name) + edge = gv.edge (graph, class_name, base_name) + gv.setv (edge, 'arrowhead', 'normal') + gv.setv (edge, 'color', SUPER_EDGE_COLOR) + # FIXME: edge style + +def add_var_edge (class_name, var_name, type_name): + check_class_node (class_name) + check_class_node (type_name) + edge = gv.edge (graph, type_name, class_name) + gv.setv (edge, 'headport', "%s:e" % var_name) + gv.setv (edge, 'arrowhead', 'diamond') + gv.setv (edge, 'color', VAR_EDGE_COLOR) + # FIXME: edge style + +def add_use_edge (class_name, base_name): + check_class_node (class_name) + check_class_node (base_name) + edge = gv.edge (graph, class_name, base_name) + gv.setv (edge, 'arrowhead', 'box') + gv.setv (edge, 'color', USE_EDGE_COLOR) + # FIXME: edge style + +for class_name in classes: + if is_excluded (class_name): + continue + for (var_name, type_name) in vars_dict[class_name]: + if class_name not in var_fields_dict: + var_fields_dict[class_name] = [] + method_fields_dict[class_name] = [] + var_fields_dict[class_name].append (var_name) + for method_name in methods_dict[class_name]: + if class_name not in var_fields_dict: + var_fields_dict[class_name] = [] + method_fields_dict[class_name] = [] + method_fields_dict[class_name].append (method_name) + +for class_name in classes: + if is_excluded (class_name): + continue + for (var_name, type_name) in vars_dict[class_name]: + if not type_name or is_excluded (type_name): + continue + add_var_edge (class_name, var_name, type_name) + for base_name in bases_dict[class_name]: + if is_excluded (base_name): + continue + add_super_edge (class_name, base_name) + for used_name in uses_dict[class_name]: + if is_excluded (used_name): + continue + add_use_edge (class_name, used_name) + +for class_name in classes: + for exp in forces: + if exp.match (class_name): + check_class_node (class_name) + break + +gv.layout (graph, 'dot') +gv.render (graph, 'png', output) |