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