Commit e4c4286a authored by Romain Casati's avatar Romain Casati
Browse files

Better API for rcviz.

parent abfd3e1f
from .rcviz import viz as _viz
from .rcviz import CallGraph as _CallGraph
from .rcviz import viz
__all__ = ["render", "graph"]
_cg = _CallGraph()
def render(*args, **kwargs):
return _cg.render(*args, **kwargs)
def graph(*args, **kwargs):
return _cg.graph(*args, **kwargs)
viz = _viz(_cg)
__all__ = ["viz"]
......@@ -2,70 +2,33 @@
# Copyright (c) Ran Dugal 2014
# Licensed under the GPLv2, which is available at
# http://www.gnu.org/licenses/gpl-2.0.html
from __future__ import print_function
# Rewriten for Basthon by Romain Casati and licenced under the same license
import inspect
import logging
import copy
import os
from pathlib import Path
import graphviz as gviz
import __main__
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
try:
__IPYTHON__ = get_ipython()
import ipykernel.zmqshell
import IPython.terminal.interactiveshell
#if float(gviz.__version__[0:3]) < 1.4:
# raise RuntimeError("IPython used with pygraphviz < 1.4 will experience "
# "some bugs")
#from IPython.display import Image
except NameError:
__IPYTHON__ = None
try:
import basthon
__BASTHON__ = basthon
except ImportError:
__BASTHON__ = None
try:
__main__file__ = inspect.getfile(__main__)
except TypeError:
if __IPYTHON__ is not None:
__main__file__ = "<ipython-input-"
if __BASTHON__ is not None:
__main__file__ = "<input>"
def trim_stack(stack):
if __IPYTHON__ or __BASTHON__:
for i in range(len(stack) - 1, -1, -1):
if os.path.basename(stack[i].filename).startswith(__main__file__):
break
return stack[:i+1]
else:
return stack
for i in range(len(stack) - 1, -1, -1):
if os.path.basename(stack[i].filename).startswith("<input>"):
break
return stack[:i+1]
class CallGraph(object):
'''singleton class that stores global graph data
draw graph using pygraphviz
'''class that stores global graph data draw graph using pygraphviz
'''
def __init__(self, filename="callgraph.png"):
self._callers = {} # caller_fn_id : NodeData
def __init__(self):
self._callers = {} # caller_fn_id : NodeData
self._counter = 1 # track call order
self._unwindcounter = 1 # track unwind order
self._frames = [] # keep frame objects reference
self._graph = None
self.filename = filename
def reset(self):
self.__init__()
self._unwindcounter = 1 # track unwind order
self._frames = [] # keep frame objects reference
def get_callers(self):
return self._callers
......@@ -85,29 +48,8 @@ class CallGraph(object):
def get_frames(self):
return self._frames
def render(self, show_null_returns=True):
self._render_graph(show_null_returns)
if not __IPYTHON__ or isinstance(__IPYTHON__, IPython.terminal.interactiveshell.TerminalInteractiveShell):
print("callviz: rendered to %s" % (self.filename))
self._graph.render(filename=self.filename)
elif isinstance(__IPYTHON__, ipykernel.zmqshell.ZMQInteractiveShell) or __BASTHON__ is not None:
from IPython.display import display
display(self._graph)
def graph(self, show_null_returns=True):
self._render_graph(show_null_returns)
return self._graph
def _render_graph(self, show_null_returns=True):
if self._graph:
self.reset()
filename = self.filename
p = Path(filename)
ext = p.suffix[1:] if p.suffix else "png"
filename = filename[:-len(ext)-1] if p.suffix else filename
g = gviz.Digraph(format=ext)
def callgraph(self, show_null_returns=True):
g = gviz.Digraph()
g.graph_attr['label'] = 'nodes=%s' % len(self._callers)
g.graph_attr['fontname'] = "helvetica"
g.node_attr['fontname'] = "helvetica"
......@@ -160,12 +102,10 @@ class CallGraph(object):
prev_node = child_node
g.subgraph(sg)"""
self._graph = g
return g
def _repr_svg_(self):
svg = self.render()._repr_svg_()
self.reset()
return svg
return self.callgraph._repr_svg_()
class NodeData(object):
......@@ -177,7 +117,7 @@ class NodeData(object):
self.ret = _ret
self.child_methods = _childmethods # [ (method, gcounter) ]
self.auxdata = {} # user assigned track data
self.auxdata = {} # user assigned track data
def __str__(self):
return "%s -> child_methods: %s" % (self.nodestr(), self.child_methods)
......@@ -187,93 +127,76 @@ class NodeData(object):
def argstr(self):
s_args = ",".join([str(arg) for arg in self.args])
s_kwargs = ",".join([(str(k), str(v))
s_kwargs = ",".join([(str(k), str(v))
for (k, v) in self.kwargs.items()])
return "%s%s" % (s_args, s_kwargs)
class viz(object):
"""decorator to construct the call graph with args
and return values as labels
"""
def viz(f, *args, **kwargs):
_callgraph = None
_callcount = 0 # incremented and decremented at each call
def __init__(self, callgraph, *args, **kwargs):
"""
If there are decorator arguments, the function
to be decorated is not passed to the constructor!
"""
self._verbose = False
if not isinstance(callgraph, CallGraph):
raise ValueError("@viz decorator must be called with a CallGraph instance")
self.callgraph = callgraph
def track(self, **kwargs):
def track(**kwargs):
fullstack = trim_stack(inspect.stack())
call_frame_id = id(fullstack[2][0])
g_callers = self.callgraph.get_callers()
g_callers = callgraph.get_callers()
node = g_callers.get(call_frame_id)
if node:
node.auxdata.update(copy.deepcopy(kwargs))
def __call__(self, f, *args, **kwargs):
"""
With decorator arguments, __call__() is only called
once, as part of the decoration process! You can only give
it a single argument, which is the function object.
"""
def callgraph(*args, **kwargs):
nonlocal _callgraph
if _callgraph is not None:
return _callgraph.callgraph(*args, **kwargs)
def wrapped_f(*args, **kwargs):
# Expected parameters for the function being wrapped
g_callers = self.callgraph.get_callers()
g_frames = self.callgraph.get_frames()
def wrapped_f(*args, **kwargs):
nonlocal _callgraph, _callcount
# find the caller frame, and add self as a child node
caller_frame_id = None
if _callcount == 0:
_callgraph = CallGraph()
fullstack = trim_stack(inspect.stack())
_callcount += 1
if self._verbose:
logging.debug("Full Stack:")
for stack in fullstack:
logging.debug("\t" + str(stack))
# Expected parameters for the function being wrapped
g_callers = _callgraph.get_callers()
g_frames = _callgraph.get_frames()
if len(fullstack) > 2:
caller_frame_id = id(fullstack[2][0])
# find the caller frame, and add self as a child node
caller_frame_id = None
if self._verbose:
logging.debug("Caller Frame: %s %s" % (caller_frame_id,
fullstack[2]))
fullstack = trim_stack(inspect.stack())
this_frame_id = id(fullstack[0][0])
if self._verbose:
logging.info("This Frame: %s %s" % (this_frame_id, fullstack[0]))
if len(fullstack) > 2:
caller_frame_id = id(fullstack[2][0])
if this_frame_id not in g_frames:
g_frames.append(fullstack[0][0])
this_frame_id = id(fullstack[0][0])
if this_frame_id not in g_callers.keys():
g_callers[this_frame_id] = NodeData(args, kwargs,
f.__name__,
None, [])
if this_frame_id not in g_frames:
g_frames.append(fullstack[0][0])
edgeinfo = None
if caller_frame_id:
edgeinfo = [this_frame_id, self.callgraph.get_counter()]
g_callers[caller_frame_id].child_methods.append(edgeinfo)
self.callgraph.increment()
if this_frame_id not in g_callers.keys():
g_callers[this_frame_id] = NodeData(args, kwargs,
f.__name__,
None, [])
edgeinfo = None
if caller_frame_id:
edgeinfo = [this_frame_id, _callgraph.get_counter()]
g_callers[caller_frame_id].child_methods.append(edgeinfo)
_callgraph.increment()
ret = f(*args, **kwargs)
g_callers[this_frame_id].ret = copy.deepcopy(ret)
ret = f(*args, **kwargs)
g_callers[this_frame_id].ret = copy.deepcopy(ret)
if self._verbose:
logging.debug('Unwinding Frame ID: %s' % this_frame_id)
if edgeinfo:
edgeinfo.append(_callgraph.get_unwindcounter())
_callgraph.increment_unwind()
if edgeinfo:
edgeinfo.append(self.callgraph.get_unwindcounter())
self.callgraph.increment_unwind()
_callcount -= 1
return ret
return ret
wrapped_f.track = self.track
return wrapped_f
wrapped_f.track = track
wrapped_f.callgraph = callgraph
return wrapped_f
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment