summaryrefslogtreecommitdiff
path: root/umlpy.py
diff options
context:
space:
mode:
Diffstat (limited to 'umlpy.py')
-rw-r--r--umlpy.py311
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)