diff --git a/.gitignore b/.gitignore index 2523144..6b42ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ dist/ *.pyc __pycache__ +.idea + diff --git a/.travis.yml b/.travis.yml index 855cc08..d3c35bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,18 @@ language: python: - "2.7" - "3.3" + - "3.4" - "pypy" +# pytest does not support python 3.5 +# https://bitbucket.org/pytest-dev/pytest/pull-request/296/astcall-signature-changed-on-35 +# - "nightly" + +matrix: + allow_failures: + - python: + - "pypy" + - python: + - "nigthly" env: global: @@ -16,8 +27,8 @@ before_install: - sudo apt-get install -qq graphviz install: - - "pip install -r requirements/development.txt --use-mirrors" - - "if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then make 2to3; fi" + - "pip install -r requirements/development.txt" + - "if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]] || [[ $TRAVIS_PYTHON_VERSION == '3.4' ]] || [[ $TRAVIS_PYTHON_VERSION == 'nightly' ]]; then make 2to3; fi" script: - make tests diff --git a/MANIFEST.in b/MANIFEST.in index db3080b..3c8de00 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,6 @@ include LICENSE include README.rst include requirements include test -include docs +recursive-include docs * include examples/*.py include man/pycallgraph.1 diff --git a/README.rst b/README.rst index f4c5390..8db8fcb 100644 --- a/README.rst +++ b/README.rst @@ -3,10 +3,14 @@ Python Call Graph Welcome! Python Call Graph is a `Python `_ module that creates `call graph `_ visualizations for Python applications. -.. image:: https://travis-ci.org/gak/pycallgraph.svg +.. image:: https://img.shields.io/travis/gak/pycallgraph.svg :target: https://travis-ci.org/gak/pycallgraph -.. image:: https://coveralls.io/repos/gak/pycallgraph/badge.svg?branch=develop +.. image:: https://img.shields.io/coveralls/gak/pycallgraph/develop.svg :target: https://coveralls.io/r/gak/pycallgraph?branch=develop +.. image:: https://img.shields.io/pypi/v/pycallgraph.svg + :target: https://crate.io/packages/pycallgraph/ +.. image:: https://img.shields.io/pypi/dm/pycallgraph.svg + :target: https://crate.io/packages/pycallgraph Screenshots =========== @@ -23,7 +27,7 @@ Click on the images below to see a larger version and the source code that gener Project Status ============== -The latest version is **1.0.1** which was released on 2013-09-17, and is a backwards incompatbile from the previous release. +The latest version is **1.0.1** which was released on 2013-09-17, and is a backwards incompatible from the previous release. The `project lives on GitHub `_, where you can `report issues `_, contribute to the project by `forking the project `_ then creating a `pull request `_, or just `browse the source code `_. @@ -62,6 +66,34 @@ A simple use of the API is:: with PyCallGraph(output=GraphvizOutput()): code_to_profile() +Use decorators for an even more simple use of the API:: + + from pycallgraph.decorators import trace + + @trace("path/to/output.png") + def main(): + code_to_profile() + + main() + +Or decorate a specific function inside your code you want to profile:: + + from pycallgraph.decorators import trace + + @trace("path/to/output.png") + def function_1(): + do_stuff + + def function_2(): + do_stuff + + def code_to_profile(): + function_1() + function_2() + + code_to_profile() + + Documentation ============= diff --git a/docs/guide/command_line_usage.rst b/docs/guide/command_line_usage.rst index 67e4fb9..4444a56 100644 --- a/docs/guide/command_line_usage.rst +++ b/docs/guide/command_line_usage.rst @@ -15,14 +15,14 @@ Description pycallgraph is a program that creates call graph visualization from Python scripts. -*OUTPUT_MODE* can be one of graphviz, gephi and json. *python_file.py* is a python script that will be traced and afterwards, a call graph visualization will be generated. +*OUTPUT_MODE* can be one of graphviz or gephi. *python_file.py* is a python script that will be traced and afterwards, a call graph visualization will be generated. General Arguments ----------------- .. cmdoption:: - A choice of graphviz, gephi and json. + A choice of graphviz or gephi. .. cmdoption:: -h, --help diff --git a/docs/guide/intro.rst b/docs/guide/intro.rst index 3b43d8e..85dedb6 100644 --- a/docs/guide/intro.rst +++ b/docs/guide/intro.rst @@ -3,7 +3,7 @@ Intro Python Call Graph was made to be a visual profiling tool for Python applications. It uses a debugging Python function called `sys.set_trace() `_ which makes a callback every time your code enters or leaves function. This allows Python Call Graph to track the name of every function called, as well as which function called which, the time taken within each function, number of calls, etc. -It is able to generate different types of :ref:`outputs and visualizations `. Initially Python Call Graph was only used to generate DOT files for `GraphViz `_, and as of version 1.0.0, it can also generate JSON files, and GDF files for Gephi. Creating :ref:`custom outputs ` is fairly easy by subclassing the :ref:`Output ` class. +It is able to generate different types of :ref:`outputs and visualizations `. Initially Python Call Graph was only used to generate DOT files for `GraphViz `_, and as of version 1.0.0, it can also generate GDF files for Gephi. Creating :ref:`custom outputs ` is fairly easy by subclassing the :ref:`Output ` class. You can either use the :ref:`command-line interface ` for a quick visualization of your Python script, or the :ref:`pycallgraph module ` for more fine-grained settings. diff --git a/examples/graphviz/example_with_submodules/__init__.py b/examples/graphviz/example_with_submodules/__init__.py new file mode 100644 index 0000000..22f9d83 --- /dev/null +++ b/examples/graphviz/example_with_submodules/__init__.py @@ -0,0 +1,3 @@ +from .example_with_submodules import main + +__all__ = [main] diff --git a/examples/graphviz/example_with_submodules/example_with_submodules.py b/examples/graphviz/example_with_submodules/example_with_submodules.py new file mode 100644 index 0000000..2fb6d04 --- /dev/null +++ b/examples/graphviz/example_with_submodules/example_with_submodules.py @@ -0,0 +1,13 @@ +from submodule_one import SubmoduleOne +from submodule_two import SubmoduleTwo + + +def main(): + s1 = SubmoduleOne() + s1.report() + + s2 = SubmoduleTwo() + s2.report() + +if __name__ == "__main__": + main() diff --git a/examples/graphviz/example_with_submodules/helpers.py b/examples/graphviz/example_with_submodules/helpers.py new file mode 100644 index 0000000..6d8bd98 --- /dev/null +++ b/examples/graphviz/example_with_submodules/helpers.py @@ -0,0 +1,2 @@ +def helper(something): + return something diff --git a/examples/graphviz/example_with_submodules/submodule_one.py b/examples/graphviz/example_with_submodules/submodule_one.py new file mode 100644 index 0000000..99017cb --- /dev/null +++ b/examples/graphviz/example_with_submodules/submodule_one.py @@ -0,0 +1,6 @@ +class SubmoduleOne(object): + def __init__(self): + self.one = 1 + + def report(self): + return self.one diff --git a/examples/graphviz/example_with_submodules/submodule_two.py b/examples/graphviz/example_with_submodules/submodule_two.py new file mode 100644 index 0000000..d3c0ac6 --- /dev/null +++ b/examples/graphviz/example_with_submodules/submodule_two.py @@ -0,0 +1,9 @@ +from helpers import helper + + +class SubmoduleTwo(object): + def __init__(self): + self.two = 2 + + def report(self): + return helper(self.two) diff --git a/examples/graphviz/grouper.py b/examples/graphviz/grouper.py new file mode 100755 index 0000000..312fad2 --- /dev/null +++ b/examples/graphviz/grouper.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +''' +This example demonstrates the use of grouping. +''' +from pycallgraph import PyCallGraph +from pycallgraph import Config +from pycallgraph import GlobbingFilter +from pycallgraph import Grouper +from pycallgraph.output import GraphvizOutput +import example_with_submodules + + +def run(name, trace_grouper=None, config=None, comment=None): + if not config: + config = Config() + + config.trace_filter = GlobbingFilter() + + if trace_grouper is not None: + config.trace_grouper = trace_grouper + + graphviz = GraphvizOutput() + graphviz.output_file = 'grouper-{}.png'.format(name) + if comment: + graphviz.graph_attributes['graph']['label'] = comment + + with PyCallGraph(config=config, output=graphviz): + example_with_submodules.main() + + +def group_none(): + run( + 'without', + comment='Default grouping.' + ) + + +def group_some(): + trace_grouper = Grouper(groups=[ + 'example_with_submodules.submodule_one.*', + 'example_with_submodules.submodule_two.*', + 'example_with_submodules.helpers.*', + ]) + + run( + 'with', + trace_grouper=trace_grouper, + comment='Should assign groups to the two submodules.', + ) + + +def group_methods(): + trace_grouper = Grouper(groups=[ + 'example_with_submodules.*.report', + ]) + + run( + 'methods', + trace_grouper=trace_grouper, + comment='Should assign a group to the report methods.', + ) + + +def main(): + group_none() + group_some() + group_methods() + + +if __name__ == '__main__': + main() diff --git a/man/pycallgraph.1 b/man/pycallgraph.1 index a46afa3..01a0876 100644 --- a/man/pycallgraph.1 +++ b/man/pycallgraph.1 @@ -64,12 +64,12 @@ pycallgraph [\fIOPTION\fP]... \fIOUTPUT_MODE\fP [\fIOUTPUT_OPTIONS\fP] \fIpython .sp pycallgraph is a program that creates call graph visualization from Python scripts. .sp -\fIOUTPUT_MODE\fP can be one of graphviz, gephi and json. \fIpython_file.py\fP is a python script that will be traced and afterwards, a call graph visualization will be generated. +\fIOUTPUT_MODE\fP can be one of graphviz or gephi. \fIpython_file.py\fP is a python script that will be traced and afterwards, a call graph visualization will be generated. .SH GENERAL ARGUMENTS .INDENT 0.0 .TP .B -A choice of graphviz, gephi and json. +A choice of graphviz or gephi. .UNINDENT .INDENT 0.0 .TP diff --git a/pycallgraph/__init__.py b/pycallgraph/__init__.py index fa03ac3..644e4ea 100644 --- a/pycallgraph/__init__.py +++ b/pycallgraph/__init__.py @@ -14,8 +14,13 @@ from .pycallgraph import PyCallGraph from .exceptions import PyCallGraphException +try: + from . import decorators +except Exception: + import decorators from .config import Config from .globbing_filter import GlobbingFilter +from .grouper import Grouper from .util import Util from .color import Color from .color import ColorException diff --git a/pycallgraph/config.py b/pycallgraph/config.py index 5911fef..2bce930 100755 --- a/pycallgraph/config.py +++ b/pycallgraph/config.py @@ -3,6 +3,7 @@ from .output import outputters from .globbing_filter import GlobbingFilter +from .grouper import Grouper class Config(object): @@ -31,6 +32,9 @@ def __init__(self, **kwargs): include=['*'], ) + # Grouping + self.trace_grouper = Grouper() + self.did_init = True # Update the defaults with anything from kwargs diff --git a/pycallgraph/decorators.py b/pycallgraph/decorators.py new file mode 100644 index 0000000..ad415b3 --- /dev/null +++ b/pycallgraph/decorators.py @@ -0,0 +1,20 @@ +import functools + +from .pycallgraph import PyCallGraph +from .output import GraphvizOutput + + +def trace(output=None, config=None): + def inner(func): + @functools.wraps(func) + def exec_func(*args, **kw_args): + + graphviz = GraphvizOutput() + graphviz.output_file = output + + with(PyCallGraph(output=graphviz, config=config)): + return func(*args, **kw_args) + + return exec_func + + return inner diff --git a/pycallgraph/grouper.py b/pycallgraph/grouper.py new file mode 100644 index 0000000..6fb6035 --- /dev/null +++ b/pycallgraph/grouper.py @@ -0,0 +1,26 @@ +from fnmatch import fnmatch + + +class Grouper(object): + '''Group module names. + + By default, objects are grouped by their top-level module name. Additional + groups can be specified with the groups list and all objects will be + matched against it. + ''' + + def __init__(self, groups=None): + if groups is None: + groups = [] + + self.groups = groups + + def __call__(self, full_name=None): + for pattern in self.groups: + if fnmatch(full_name, pattern): + if pattern[-2:] == ".*": + # a wilcard in the middle is probably meaningful, while at + # the end, it's only noise and can be removed + return pattern[:-2] + return pattern + return full_name.split('.', 1)[0] diff --git a/pycallgraph/output/graphviz.py b/pycallgraph/output/graphviz.py index 6f10049..e875e39 100644 --- a/pycallgraph/output/graphviz.py +++ b/pycallgraph/output/graphviz.py @@ -3,6 +3,7 @@ import tempfile import os import textwrap +import subprocess as sub from ..metadata import __version__ from ..exceptions import PyCallGraphException @@ -45,6 +46,7 @@ def add_arguments(cls, subparsers, parent_parser, usage): 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.', ) @@ -99,13 +101,14 @@ def done(self): with os.fdopen(fd, 'w') as f: f.write(source) - cmd = '{} -T{} -o{} {}'.format( + cmd = '"{0}" -T{1} -o{2} {3}'.format( self.tool, self.output_type, self.output_file, temp_name ) - self.verbose('Executing: {}'.format(cmd)) + self.verbose('Executing: {0}'.format(cmd)) try: - ret = os.system(cmd) + proc = sub.Popen(cmd, stdout=sub.PIPE, stderr=sub.PIPE, shell=True) + ret, output = proc.communicate() if ret: raise PyCallGraphException( 'The command "%(cmd)s" failed with error ' @@ -113,7 +116,7 @@ def done(self): finally: os.unlink(temp_name) - self.verbose('Generated {} with {} nodes.'.format( + self.verbose('Generated {0} with {1} nodes.'.format( self.output_file, len(self.processor.func_count), )) @@ -127,16 +130,16 @@ def generate(self): digraph G {{ // Attributes - {} + {0} // Groups - {} + {1} // Nodes - {} + {2} // Edges - {} + {3} }} '''.format( @@ -153,7 +156,7 @@ def attrs_from_dict(self, d): return ', '.join(output) def node(self, key, attr): - return '"{}" [{}];'.format( + return '"{0}" [{1}];'.format( key, self.attrs_from_dict(attr), ) @@ -165,7 +168,7 @@ def edge(self, edge, attr): def generate_attributes(self): output = [] for section, attrs in self.graph_attributes.iteritems(): - output.append('{} [ {} ];'.format( + output.append('{0} [ {1} ];'.format( section, self.attrs_from_dict(attrs), )) return output diff --git a/pycallgraph/output/output.py b/pycallgraph/output/output.py index 9660d58..662d563 100644 --- a/pycallgraph/output/output.py +++ b/pycallgraph/output/output.py @@ -24,7 +24,8 @@ def set_config(self, config): the output module config variables. ''' for k, v in config.__dict__.iteritems(): - if hasattr(self, k) and callable(getattr(self, k)): + if hasattr(self, k) and \ + callable(getattr(self, k)): continue setattr(self, k, v) @@ -52,7 +53,7 @@ def node_label(self, node): return r'\n'.join(parts).format(node) def edge_label(self, edge): - return '{}'.format(edge.calls.value) + return '{0}'.format(edge.calls.value) def sanity_check(self): '''Basic checks for certain libraries or external applications. Raise @@ -93,7 +94,7 @@ def ensure_binary(self, cmd): return raise PyCallGraphException( - 'The command "{}" is required to be in your path.'.format(cmd)) + 'The command "{0}" is required to be in your path.'.format(cmd)) def normalize_path(self, path): regex_user_expand = re.compile('\A~') diff --git a/pycallgraph/pycallgraph.py b/pycallgraph/pycallgraph.py index 1a03a50..6af5428 100644 --- a/pycallgraph/pycallgraph.py +++ b/pycallgraph/pycallgraph.py @@ -7,12 +7,11 @@ class PyCallGraph(object): - def __init__(self, output=None, config=None): '''output can be a single Output instance or an iterable with many of them. Example usage: - PyCallGraph(config=Config(), output=GraphvizOutput()) + PyCallGraph(output=GraphvizOutput(), config=Config()) ''' locale.setlocale(locale.LC_ALL, '') diff --git a/pycallgraph/tracer.py b/pycallgraph/tracer.py index 17e9286..ffcab15 100644 --- a/pycallgraph/tracer.py +++ b/pycallgraph/tracer.py @@ -290,20 +290,17 @@ def __getstate__(self): return odict - def group(self, name): - return name.split('.', 1)[0] - def groups(self): grp = defaultdict(list) for node in self.nodes(): - grp[self.group(node.name)].append(node) + grp[node.group].append(node) for g in grp.iteritems(): yield g def stat_group_from_func(self, func, calls): stat_group = StatGroup() stat_group.name = func - stat_group.group = self.group(func) + stat_group.group = self.config.trace_grouper(func) stat_group.calls = Stat(calls, self.func_count_max) stat_group.time = Stat(self.func_time.get(func, 0), self.func_time_max) stat_group.memory_in = Stat( diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5e40900 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = 1 diff --git a/test/test_decorators.py b/test/test_decorators.py new file mode 100644 index 0000000..8392a4c --- /dev/null +++ b/test/test_decorators.py @@ -0,0 +1,39 @@ +import pytest + +import pycallgraph +from pycallgraph import PyCallGraphException +from pycallgraph.output import GephiOutput, GraphvizOutput + + +@pycallgraph.decorators.trace(output=GraphvizOutput()) +def print_something(): + print("hello") + + +@pycallgraph.decorators.trace(output=GephiOutput()) +def print_foo(): + print("foo") + + +@pycallgraph.decorators.trace() +def print_bar(): + print("bar") + + +def test_trace_decorator_graphviz_output(): + print_something() + + +def test_trace_decorator_gephi_output(): + print_foo() + + +def test_trace_decorator_parameter(): + with pytest.raises(PyCallGraphException): + print_bar() + + +if __name__ == "__main__": + test_trace_decorator_graphviz_output() + test_trace_decorator_gephi_output() + test_trace_decorator_parameter()