PK`LrMV//callgraph/__init__.py"""This package defines decorators and IPython magic to display a dynamic call graph.""" __version__ = '1.0.0' __all__ = ['load_ipython_extension', 'decorator', 'CallGraphRecorder'] from .extension import load_ipython_extension from .decorator import decorator from .recorder import CallGraphRecorder PKV*L~qcallgraph/decorator.pyfrom .recorder import CallGraphRecorder def decorator(fn=None, recorder=None, label_returns=False, graph_attrs=None): """Decorator that wraps a function with instrumentation to record calls to it, for use in constructing a call graph. Parameters ---------- recorder : CallGraphRecorder, optional An CallGraphRecorder. If this is not supplied, a new recorder is created with the specified values for ``label_returns`` and ``graph_attrs``, and attached to the decorated function as ``fn.__callgraph__``. label_returns : bool If true, arrows are draw from callee to caller, and labeled with the return value. graph_attrs : dict Graphviz graph attributes. These are passed to :meth:`graphviz.Digraph.attr`. A new :class:`graphviz.Digraph`. Examples -------- :: import callgraph.decorator as callgraph @callgraph() def nchoosek(n, k): if k == 0: return 1 if n == k: return 1 return nchoosek(n - 1, k - 1) + nchoosek(n - 1, k) """ rec = recorder or CallGraphRecorder(label_returns=label_returns, graph_attrs=graph_attrs) def graphing_decorator(fn): wrapper = rec.wrap(fn) if not recorder: wrapper.__callgraph__ = rec.graph return wrapper # The following allows the decorator to be used as either `decorator` or # `decorator()`. The examples don't currently demonstrate this, and it # may be a bad idea. return graphing_decorator(fn) if fn else graphing_decorator PKUQLE<callgraph/extension.py"Jupyter IPython magic to display a dynamic call graph." import ast from IPython.core.magic import Magics, line_cell_magic, magics_class, needs_local_scope from IPython.display import SVG, display from IPython.testing.skipdoctest import skip_doctest from .instrumentor import CallGraphInstrumentor from .recorder import CallGraphRecorder @magics_class class CallGraphMagics(Magics): @skip_doctest @line_cell_magic @needs_local_scope def callgraph(self, line, local_ns=None): """Display the dynamic call graph for a Python statement or expression. Usage:: %callgraph nchoosek(5, 2) This magic instruments global functions that are named in the statement. On completion, the functions are restored to their original values. The magic knows about functions that are decorated with :func:`functools.lru_cache`. Two calls with the same arguments to a cached function will display as the same node. For simplicity, "same" to the instrumentation just means same string representation. It also ignores the `maxsize` and `typed` arguments to :func:`~functools.lru_cache`. Options: -h: display the graph “horizontally”, with function calls running from left to right. -r: reverse the graph (display arrows from callee to caller). Label the arrows with the return values. -w: max width of the graph --no-clear: Don't clear :func:`~functools.lru_cache` caches before execution. Examples -------- :: from functools import lru_cache @lru_cache() def nchoosek(n, k): if k == 0: return 1 if n == k: return 1 return nchoosek(n - 1, k - 1) + nchoosek(n - 1, k) %callgraph nchoosek(5, 2) %callgraph nchoosek(5, 2); nchoosek(5, 3) %callgraph -r nchoosek(5, 2) """ # Some constants. filename = '' mode = 'exec' opts, stmt = self.parse_options(line, 'w:ehr', 'no-clear', posix=False) # Parse the options. Create a call graph recorder with those options. options = {'graph_attrs': {}} if 'e' in opts: options['equal'] = True if 'r' in opts: options['label_returns'] = True if 'h' in opts: options['graph_attrs']['rankdir'] = 'LR' if 'w' in opts: options['graph_attrs']['size'] = '{},'.format(opts['w']) rec = CallGraphRecorder(**options) # Parse the statement. Collect calls to instrument. tree = ast.parse(stmt, filename=filename, mode=mode) calls = [n for n in ast.walk(tree) if isinstance(n, ast.Call)] fn_names = [n.func.id for n in calls if isinstance(n.func, ast.Name)] # Clear lru_cache caches. We actually invoke the cache_clear method of # any function with a callable cache_clear attribute. This affords the # opportunity to use this mechanism on other memoization decorators, # although I'm not aware of any at the moment. if 'no-clear' not in opts: fns = (local_ns[name] for name in fn_names if name in local_ns) clear_fns = filter( None, (getattr(fn, 'cache_clear', None) for fn in fns)) list(c() for c in clear_fns if hasattr(c, '__call__')) # For now, only global variables (`a(x)`) are instrumented, not # attributes (`a.b(x)`). The following would collect attributes # ("qualified" function calls): # # [n.func for n in calls if isinstance(n.func, ast.Attribute)] with CallGraphInstrumentor(fn_names, recorder=rec, local_ns=local_ns) as instr: exec(compile(tree, filename=filename, mode=mode), local_ns) display(SVG(data=instr.graph._repr_svg_())) def load_ipython_extension(ipython): """Register the IPython magic. Jupyter / IPython calls this when the extension is loaded. You don't need to. See the package documentation for instructions on how to tell Jupyter to load the extension. """ ipython.register_magics(CallGraphMagics) PK,o6L7Krrcallgraph/instrumentor.pyfrom .recorder import CallGraphRecorder class CallGraphInstrumentor(object): """A context manager that instruments (dynamically decorators) a collection of functions in a namespace on entry, and restores their saved values on exit.""" def __init__(self, names, recorder=None, local_ns=None): self.recorder = recorder or CallGraphRecorder() self._names = names self._ns = local_ns or globals() self._restore = None def __enter__(self): ns = self._ns fns = {n: ns[n] for n in self._names if ns[n].__call__} for name, fn in fns.items(): ns[name] = self.recorder.wrap(fn) self._restore = fns return self.recorder def __exit__(self, _type, _value, _traceback): ns = self._ns fns = self._restore for name, fn in fns.items(): ns[name] = fn PKPL DPPcallgraph/recorder.pyimport functools from functools import wraps from itertools import starmap from graphviz import Digraph class CallGraphRecorder(object): """Record function calls into a Graphviz diagraph. Attributes ---------- graph : Digraph A :class:`graphviz.Digraph`. """ def __init__(self, equal=False, label_returns=False, graph_attrs=None): self.graph = Digraph(format='svg', strict=True) if graph_attrs: self.graph.graph_attr.update(**graph_attrs) self._options = {'equal': equal, 'label_returns': label_returns} self._next_call_idx = 0 self._callers = [] def wrap(self, fn): """A decorator that wraps fn with instrumentation to record calls to it. You probably want :func:`decorator` instead. """ @wraps(fn) def wrapper(*args, **kwargs): with self.record(fn, args, kwargs) as record_return: result = fn(*args, **kwargs) record_return(result) return result return wrapper def _record(self, caller_id, call_id, fn, args, kwargs, result): "Record a function call." graph = self.graph label_returns = self._options['label_returns'] label = "{}({}{}{})".format(fn.__name__, ', '.join(map(repr, args)), ', ' if args and kwargs else '', ', '.join(starmap("{}={}".format, kwargs.items()))) if not (label_returns and caller_id): label += " ↦ {}".format(result) graph.node(call_id, label=label) if caller_id: if label_returns: graph.edge(caller_id, call_id, label=str(result), dir='back') else: graph.edge(caller_id, call_id) else: graph.node(call_id, penwidth='3') def record(self, fn, args, kwargs): """Return a context manager that records a function call. Returns ------- CallGraphCallRecorder A context manager that records a function call. Examples -------- :: with recorder.record(fn, args, kwargs) as record_return: result = fn(*args, **kwargs) record_return(result) """ return CallGraphCallRecorder(self, fn, args, kwargs) def _next_call_id(self, fn, args, kwargs): # if it's memoized, use the function name and arguments as a node id. if self._options['equal'] or isinstance(fn, functools._lru_cache_wrapper): return '{}{}{}'.format(getattr(fn, '__name__', str(fn)), args, kwargs) self._next_call_idx += 1 return str(self._next_call_idx) class CallGraphCallRecorder(object): """A context manager that records a function call on its associated CallGraphRecorder. """ __slots__ = ['_recorder', '_fn', '_args', '_kwargs'] def __init__(self, recorder, fn, args, kwargs): self._recorder = recorder self._fn, self._args, self._kwargs = fn, args, kwargs def __enter__(self): fn, args, kwargs = self._fn, self._args, self._kwargs recorder = self._recorder stack = recorder._callers caller_id = (stack[-1] if stack else None) call_id = recorder._next_call_id(fn, args, kwargs) stack.append(call_id) def record_return(result): recorder._record(caller_id, call_id, fn, args, kwargs, result) return record_return def __exit__(self, _type, _value, _traceback): self._recorder._callers.pop() PK b"L0*88!callgraph-1.0.0.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2018 Oliver Steele Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HNOcallgraph-1.0.0.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,zd&Y)r$[)T&UrPK!HaK.d"callgraph-1.0.0.dist-info/METADATAXkn7Sn Hkb 1>&N'-RĊܒ\*z3dɈGGZsx0V2Sr Ќ+0Z);a'. n }V<[񅠹RXδNKyɳ[|!34ͥRNm 8DEkb']y ]:W$-tkP"M{,3QZ~y.y LB*^z4*AʃFJ&4F܄~{9<63r}PN.z@.uʹhI~7uuLPK.rz^R@Gb- o>36Bz^UFL^wX>T.BU^P Ֆ?3rR𘗋Q, wsHZt }6& w#}?as,:eńB74ݯNuaM;+;Xz:>`;7o7ၲ|MxhszC4czp$G @{5(4틾;&Kݕ Wv2, w_ǀ A3A茻a҂$:w2cZɊmpd2,VAi>QSQCQ-l.WΓp?ҕHz ]sЫ={c  (!+kye`xb! (.n ^?Nyc) .$\. \*z$Es377piŪm22zmV;U.@ΩtlLϥOQx/P z1UՓ$d&cx0<_.Eu!/!<@m-I). xھ o )Bɒ OӃ`ɥ="5/*0 :qR ~v@_@nf^ҝM)[K|89r?;ـۑƒEasځ؁̤F.ԧ<pscRk#ʵpLhgڵD>(8#蹦;R;Toj @*Z"7in%\\)kuAzG-u`.Z%WYY,]JrTJb xY3"% e,g_"y~ތfmv V "/Nm?PK!Hһ callgraph-1.0.0.dist-info/RECORD}Is@{~ $la9ed \(vцƦ!)r^P|$I58I'1\R*׾B2!NA.2bL\Eط+C(m}z=Lw gl/܊+v+Nrk 8ܼ}8OB-XY K}0۲]i6Yh^/ϊZg8t-WUB-5>S^ow(X /9x#_kI0tϏ|qLpmKc2|>wdpM2i4%R/pַkilAbց.AqIۢG1idE>hSWMAUeafl~ Ұmv&>U᣺u APK`LrMV//callgraph/__init__.pyPKV*L~qbcallgraph/decorator.pyPKUQLE<callgraph/extension.pyPK,o6L7Krrcallgraph/instrumentor.pyPKPL DPPcallgraph/recorder.pyPK b"L0*88!G+callgraph-1.0.0.dist-info/LICENSEPK!HNO/callgraph-1.0.0.dist-info/WHEELPK!HaK.d"I0callgraph-1.0.0.dist-info/METADATAPK!Hһ 7callgraph-1.0.0.dist-info/RECORDPK 9