Index: scripts/pat2dot.py =================================================================== --- scripts/pat2dot.py (revision 0) +++ scripts/pat2dot.py (revision 0) @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# Copyright 2010 G. M. Bond. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +"""Script that generates a graphviz dot file that describes a PATTERN. + +Sometimes being able to visualize the exact structure of a PATTERN that has +been compiled can be useful for development and debugging purposes. This script +will output a file that describes the compiled pattern as a graph using the dot +syntax, which can then be compiled using the dot tool supplied as part of +graphviz. It will only work on fixers that actually define a pattern. + +Usage: + python pat2dot.py fixername +""" + +__author__ = "G. Matthew Bond " + +#Python imports +import os +import sys + +#Local imports +## Currently this is tied to an adapter for GvGen, but it should be easy to +## switch to a different graphviz library or even write your own. +from pat2dot import gvgraph +from lib2to3 import refactor + +def main(fixer_pkg, args=sys.argv[1:]): + if len(args) != 1: + print "Usage: pat2dot fixer_name\n" + print "Notes:\nTakes a fixer name as described by 2to3's -l option\n" + + if args[0] == "-l": + print "Available fixers for graphing:" + for fixname in refactor.get_all_fix_names(fixer_pkg): + print fixname + return 0 + + CLASS_PREFIX = "Fix" # The prefix for fixer classes + FILE_PREFIX = "fix_" # The prefix for modules with a fixer within + + target_fixer_name = fixer_pkg + ".fix_" + args[0] + try: + module = __import__(target_fixer_name, {}, {}, ["*"]) + except ImportError: + print "Couldn't find fixer " + target_fixer_name + return 0 + + #Change this out once input can be taken from argv + fix_log = [] + fix_name = target_fixer_name.rsplit(".", 1)[-1] + if fix_name.startswith(FILE_PREFIX): + fix_name = fix_name[len(FILE_PREFIX):] + parts = fix_name.split("_") + class_name = CLASS_PREFIX + "".join([p.title() for p in parts]) + try: + fix_class = getattr(module, class_name) + except AttributeError: + print "Couldn't initialize %s from %s\n" % (fix_name, class_name) + return 0 + fixer = fix_class(None, fix_log) + if fixer.pattern: + g = gvgraph.GvGraph() + fixer.pattern.graph_node(g) + g.output() + else: + print "Couldn't find a valid pattern in fixer %s" % target_fixer_name + return 0 + +if __name__ == '__main__': + sys.exit(main("lib2to3.fixes")) Property changes on: scripts/pat2dot.py ___________________________________________________________________ Added: svn:executable + * Index: scripts/pat2dot/gvgraph.py =================================================================== --- scripts/pat2dot/gvgraph.py (revision 0) +++ scripts/pat2dot/gvgraph.py (revision 0) @@ -0,0 +1,28 @@ +# Copyright 2010 G. M. Bond. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +__author__ = "G. Matthew Bond " + +import sys +import gvgen +from . import graph + +class GvGraph(graph.Graph): + + """This is an implementation of the Graph class using GvGen.""" + + def __init__(self): + """Set up the graph.""" + self.graph = gvgen.GvGen() + + def add_node(self, node_name): + """Return a new node with the caption node_name.""" + return self.graph.newItem(node_name) + + def add_edge(self, parent, child): + """Create and return a new edge between parent and child.""" + return self.graph.newLink(parent, child) + + def output(self, fp=sys.stdout): + """Output the graph in dot format to the file descriptor fd.""" + return self.graph.dot(fp) Index: scripts/pat2dot/__init__.py =================================================================== Index: scripts/pat2dot/graph.py =================================================================== --- scripts/pat2dot/graph.py (revision 0) +++ scripts/pat2dot/graph.py (revision 0) @@ -0,0 +1,32 @@ +# Copyright 2010 G. M. Bond. All Rights Reserved. +# Licensed to PSF under a Contributor Agreement. + +__author__ = "G. Matthew Bond " + +import sys + +class Graph(object): + + """ + Base class for adapting graphviz dot libraries for use in lib2to3. + + This class provides the interface that should be used for adapting a + graphviz library for use with the graph_node method of compiled patterns in + fixers. This class was added so there would be no direct dependence on any + one graphviz implementation. + """ + + def add_node(self, node_name): + """Add a node calle node_name to the graph.""" + raise NotImplementedError + + def add_edge(self, parent, child): + """Add an edge between parent and child.""" + raise NotImplementedError + + def output(self, fp=sys.stdout): + """Output the graph in dot format to the file descriptor fd.""" + raise NotImplementedError + + + Index: lib2to3/pytree.py =================================================================== --- lib2to3/pytree.py (revision 84070) +++ lib2to3/pytree.py (working copy) @@ -533,7 +533,20 @@ if nodes and self.match(nodes[0], r): yield 1, r + def node_repr(self): + """Return node information suitable for graphing""" + return "Classtype: Base Node" + def graph_node(self, graph, graph_children=True): + """Add this node, and optionally its children, to the supplied graph. + + It may be perfectly acceptable in some cases to not add a node to the + graph, so concrete subclasses must override this if they should appear + on the graph. + """ + pass + + class LeafPattern(BasePattern): def __init__(self, type=None, content=None, name=None): @@ -577,7 +590,31 @@ """ return self.content == node.value + def node_repr(self): + """Return node information suitable for graphing""" + str_name_list = ["Classtype: Leaf"] + if self.type is None: + str_name_list.append("Match: any leaf{1}") + else: + str_name_list.append("Match: %s{1}" % type_repr(self.type)) + if self.content is not None: + str_name_list.append("Match String: %s" % self.content) + if self.name is not None: + str_name_list.append("Store as: %s" % self.name) + return '\\n'.join(str_name_list) + def graph_node(self, graph, graph_children=True): + """Add node to the graph, return the new graph node. + + Since leaf nodes never have children graph_children + has no effect. + """ + #Graph self + node = graph.add_node(self.node_repr()) + #Leaf Nodes have no children + return node + + class NodePattern(BasePattern): wildcards = False @@ -637,8 +674,30 @@ if not subpattern.match(child, results): return False return True + + def node_repr(self): + """Return node information suitable for graphing""" + str_name_list = ["Classtype: Node"] + if self.type is None: + str_name_list.append("Match: any{1}") + else: + str_name_list.append("Match: %s{1}" % type_repr(self.type)) + if self.name is not None: + str_name_list.append("Store as: %s" % self.name) + return '\\n'.join(str_name_list) + def graph_node(self, graph, graph_children=True): + """Add node and children to graph, return the new graph node.""" + #Graph self + node = graph.add_node(self.node_repr()) + #Graph children + if graph_children and (self.content is not None): + for n in self.content: + child = n.graph_node(graph) + graph.add_edge(node, child) + return node + class WildcardPattern(BasePattern): """ @@ -823,7 +882,38 @@ r.update(r1) yield c0 + c1, r + def node_repr(self): + """Return node information suitable for graphing""" + str_name_list = ["Classtype: Wildcard"] + if self.content is None: + str_name_list.append("Match: any") + else: + str_name_list.append("Match: subsequence") + if self.min == self.max: + str_name_list.append("%d occurances" % self.min) + elif self.max == HUGE: + str_name_list.append("%d - MAX occurances" % self.min) + else: + str_name_list.append("%d - %d occurances" % (self.min, self.max)) + if self.name is not None: + str_name_list.append("Store as: %s" % self.name) + return '\\n'.join(str_name_list) + def graph_node(self, graph, graph_children=True): + """Add node and children to graph, return the new graph node.""" + #Graph self + this = graph.add_node(self.node_repr()) + #Graph children + if graph_children and (self.content is not None): + for sequence in self.content: + parent = graph.add_node("Sequence") + graph.add_edge(this, parent) + for node in sequence: + child = node.graph_node(graph) + graph.add_edge(parent, child) + return this + + class NegatedPattern(BasePattern): def __init__(self, content=None): @@ -858,7 +948,26 @@ return yield 0, {} + def node_repr(self): + """Return node information suitable for graphing""" + str_name_list = ["Classtype: Negated Node"] + if self.content is None: + str_name_list.append("Match: $") + else: + str_name_list.append("Match: Not Subpattern") + return '\\n'.join(str_name_list) + def graph_node(self, graph, graph_children=True): + """Add node and children to graph, return the new graph node.""" + #Graph self + node = graph.add_node(self.node_repr()) + #Graph children + if graph_children and (self.content is not None): + child = self.content.graph_node(graph) + graph.add_edge(node, child) + return node + + def generate_matches(patterns, nodes): """ Generator yielding matches for a sequence of patterns and nodes. Index: lib2to3/tests/test_pytree.py =================================================================== --- lib2to3/tests/test_pytree.py (revision 84070) +++ lib2to3/tests/test_pytree.py (working copy) @@ -471,3 +471,57 @@ r = {} self.assertTrue(pattern.match(node, r)) self.assertEqual(r["args"], [l2]) + + def test_graphing(self): + #Test Graph class + class testGraph(): + def __init__(self): + self.record = [] + self.counter = 0 + + def add_node(self, node_name): + """Add a node calle node_name to the graph.""" + self.counter += 1 + self.record.append((self.counter, 'add', node_name)) + return self.counter + + def add_edge(self, parent, child): + """Add an edge between parent and child.""" + self.counter += 1 + self.record.append((self.counter, 'edge', parent, child)) + return self.counter + + def output(self, fp=sys.stdout): + """Output the graph in dot format to the file descriptor fd.""" + return self.record + + # Build a pattern + pl = pytree.LeafPattern(100, "foo", name="pl") + pn = pytree.NodePattern(1000, [pl], name="pn") + pw = pytree.WildcardPattern([[pn], [pl, pl]], name="pw") + #Build graph + g = testGraph() + pw.graph_node(g) + expected = [ + (1, 'add', 'Classtype: Wildcard\\nMatch: subsequence\\n' + '0 - MAX occurances\\nStore as: pw'), + (2, 'add', 'Sequence'), + (3, 'edge', 1, 2), + (4, 'add', 'Classtype: Node\\nMatch: 1000{1}\\nStore as: pn'), + (5, 'add', 'Classtype: Leaf\\nMatch: 100{1}\\nMatch String: foo\\n' + 'Store as: pl'), + (6, 'edge', 4, 5), + (7, 'edge', 2, 4), + (8, 'add', 'Sequence'), + (9, 'edge', 1, 8), + (10, 'add', 'Classtype: Leaf\\nMatch: 100{1}\\nMatch String: foo\\n' + 'Store as: pl'), + (11, 'edge', 8, 10), + (12, 'add', 'Classtype: Leaf\\nMatch: 100{1}\\nMatch String: foo\\n' + 'Store as: pl'), + (13, 'edge', 8, 12)] + + self.assertEqual(g.output(), expected) + + +