Source code for pycallgraph.output.graphviz



import tempfile
import os
import textwrap
import subprocess as sub

from ..metadata import __version__
from ..exceptions import PyCallGraphException
from ..color import Color
from .output import Output


[docs] class GraphvizOutput(Output): def __init__(self, **kwargs): self.tool = 'dot' self.tool_package = 'graphviz' self.output_file = 'pycallgraph.png' self.output_type = 'png' self.font_name = 'Verdana' self.font_size = 7 self.group_font_size = 10 self.group_border_color = Color(0, 0, 0, 0.8) Output.__init__(self, **kwargs) self.prepare_graph_attributes() @classmethod def add_arguments(cls, subparsers, parent_parser, usage): defaults = cls() subparser = subparsers.add_parser( 'graphviz', help='Graphviz generation', parents=[parent_parser], usage=usage, ) subparser.add_argument( '-l', '--tool', dest='tool', default=defaults.tool, help='The tool from Graphviz to use, e.g. dot, neato, etc.', ) cls.add_output_file( subparser, defaults, 'The generated Graphviz file' ) subparser.add_argument( '-f', '--output-format', type=str, default=defaults.output_type, dest='output_type', help='Image format to produce, e.g. png, ps, dot, etc. ' 'See http://www.graphviz.org/doc/info/output.html for more.', ) subparser.add_argument( '--font-name', type=str, default=defaults.font_name, help='Name of the font to be used', ) subparser.add_argument( '--font-size', type=int, default=defaults.font_size, help='Size of the font to be used', )
[docs] def sanity_check(self): self.ensure_binary(self.tool, self.tool_package)
def prepare_graph_attributes(self): generated_message = '\\n'.join([ r'Generated by Python Call Graph v%s' % __version__, r'https://github.com/Lewiscowles1986/py-call-graph', ]) self.graph_attributes = { 'graph': { 'overlap': 'scalexy', 'fontname': self.font_name, 'fontsize': self.font_size, 'fontcolor': Color(0, 0, 0, 0.5).rgba_web(), 'label': generated_message, }, 'node': { 'fontname': self.font_name, 'fontsize': self.font_size, 'fontcolor': Color(0, 0, 0).rgba_web(), 'style': 'filled', 'shape': 'rect', }, 'edge': { 'fontname': self.font_name, 'fontsize': self.font_size, 'fontcolor': Color(0, 0, 0).rgba_web(), } }
[docs] def done(self): source = self.generate() self.debug(source) fd, temp_name = tempfile.mkstemp() with os.fdopen(fd, 'w') as f: f.write(source) # Pass an argv sequence rather than a shell command string: with # shell=True a path or option containing shell metacharacters would be # reinterpreted by the shell. command = [ self.tool, '-T{0}'.format(self.output_type), '-o{0}'.format(self.output_file), temp_name, ] self.verbose('Executing: {0}'.format(' '.join(command))) try: proc = sub.Popen( command, stdout=sub.PIPE, stderr=sub.PIPE, shell=False ) _, stderr = proc.communicate() if proc.returncode: # Surface what dot actually said, or the failure is opaque. detail = (stderr or b'').decode( 'utf-8', errors='replace').strip() message = ( 'The command "{0}" failed with exit code {1}.'.format( ' '.join(command), proc.returncode) ) if detail: message += ' Output: {0}'.format(detail) raise PyCallGraphException(message) finally: os.unlink(temp_name) self.verbose('Generated {0} with {1} nodes.'.format( self.output_file, len(self.processor.func_count), ))
[docs] def generate(self): '''Returns a string with the contents of a DOT file for Graphviz to parse. ''' indent_join = '\n' + ' ' * 12 return textwrap.dedent('''\ digraph G {{ // Attributes {0} // Groups {1} // Nodes {2} // Edges {3} }} '''.format( indent_join.join(self.generate_attributes()), indent_join.join(self.generate_groups()), indent_join.join(self.generate_nodes()), indent_join.join(self.generate_edges()), ))
def attrs_from_dict(self, d): output = [] for attr, val in list(d.items()): output.append('%s = "%s"' % (attr, val)) return ', '.join(output) def node(self, key, attr): return '"{0}" [{1}];'.format( key, self.attrs_from_dict(attr), ) def edge(self, edge, attr): return '"{0.src_func}" -> "{0.dst_func}" [{1}];'.format( edge, self.attrs_from_dict(attr), ) def generate_attributes(self): output = [] for section, attrs in list(self.graph_attributes.items()): output.append('{0} [ {1} ];'.format( section, self.attrs_from_dict(attrs), )) return output def generate_groups(self): if not self.processor.config.groups: return '' output = [] for group, nodes in self.processor.groups(): funcs = [node.name for node in nodes] funcs = '" "'.join(funcs) group_color = self.group_border_color.rgba_web() group_font_size = self.group_font_size output.append( 'subgraph "cluster_{group}" {{ ' '"{funcs}"; ' 'label = "{group}"; ' 'fontsize = "{group_font_size}"; ' 'fontcolor = "black"; ' 'style = "bold"; ' 'color="{group_color}"; }}'.format(**locals())) return output def generate_nodes(self): output = [] for node in self.processor.nodes(): attr = { 'color': self.node_color_func(node).rgba_web(), 'label': self.node_label_func(node), } output.append(self.node(node.name, attr)) return output def generate_edges(self): output = [] for edge in self.processor.edges(): attr = { 'color': self.edge_color_func(edge).rgba_web(), 'label': self.edge_label_func(edge), } output.append(self.edge(edge, attr)) return output