Technologicat / pyan

Static call graph generator. The official Python 3 version. Development repo.
GNU General Public License v2.0
329 stars 57 forks source link
call-graph python python3 python36 python37 static-code-analysis

Pyan3

Offline call graph generator for Python 3

Build Status FOSSA Status Codacy Badge PyPI - Python Version

Pyan takes one or more Python source files, performs a (rather superficial) static analysis, and constructs a directed graph of the objects in the combined source, and how they define or use each other. The graph can be output for rendering by GraphViz or yEd.

This project has 2 official repositories:

The PyPI package pyan3 is built from development

Help wanted! [November 2023]

The last major analyzer upgrades to pyan were made several years ago, for Python 3.6. If pyan has worked at all for Python 3.7+, that is pure luck.

It pains me to say, but as I am sure you all have noticed by the inactivity, I do not have the resources to keep pyan alive, nor to fix its many design issues.

Therefore:

Since there is a continuing community interest in pyan, I would like to hand over the pyan project to the community, including write access to the PyPI package.

If interested, you can contact me by posting a comment on this issue.

Background

I'm in a position that is rather peculiar for a software developer, where I have not needed a static analyzer in many years. Although as a computational scientist, I write my own research codes in Python, in practice:

Therefore, in my work, a structural analyzer such as pyan does not help much. As we all know, this is not the case in most other kinds of software engineering.

Of my Python metaprogramming projects, I am semi-actively using some parts of unpythonic. I also have some interest in keeping the mcpyrate macro expander alive, because it goes much further than earlier designs, and no one else seems to have run off with the mantle yet. But that is basically all I can afford in the foreseeable future.

Even for those two projects, as of this writing, the most recent opportunity I had to work on them was almost two years ago. So even the projects I am maintaining, currently only support up to Python 3.10.

--Technologicat

About

Example output

Defines relations are drawn with dotted gray arrows.

Uses relations are drawn with black solid arrows. Recursion is indicated by an arrow from a node to itself. Mutual recursion between nodes X and Y is indicated by a pair of arrows, one pointing from X to Y, and the other from Y to X.

Nodes are always filled, and made translucent to clearly show any arrows passing underneath them. This is especially useful for large graphs with GraphViz's fdp filter. If colored output is not enabled, the fill is white.

In node coloring, the HSL color model is used. The hue is determined by the filename the node comes from. The lightness is determined by depth of namespace nesting, with darker meaning more deeply nested. Saturation is constant. The spacing between different hues depends on the number of files analyzed; better results are obtained for fewer files.

Groups are filled with translucent gray to avoid clashes with any node color.

The nodes can be annotated by filename and source line number information.

Note

The static analysis approach Pyan takes is different from running the code and seeing which functions are called and how often. There are various tools that will generate a call graph that way, usually using a debugger or profiling trace hooks, such as Python Call Graph.

In Pyan3, the analyzer was ported from compiler (good riddance) to a combination of ast and symtable, and slightly extended.

Install

pip install pyan3

Usage

See pyan3 --help.

Example:

pyan *.py --uses --no-defines --colored --grouped --annotated --dot >myuses.dot

Then render using your favorite GraphViz filter, mainly dot or fdp:

dot -Tsvg myuses.dot >myuses.svg

Or use directly

pyan *.py --uses --no-defines --colored --grouped --annotated --svg >myuses.svg

You can also export as an interactive HTML

pyan *.py --uses --no-defines --colored --grouped --annotated --html > myuses.html

Alternatively, you can call pyan from a script

import pyan
from IPython.display import HTML
HTML(pyan.create_callgraph(filenames="**/*.py", format="html"))

Sphinx integration

You can integrate callgraphs into Sphinx. Install graphviz (e.g. via sudo apt-get install graphviz) and modify source/conf.py so that

# modify extensions
extensions = [
  ...
  "sphinx.ext.graphviz"
  "pyan.sphinx",
]

# add graphviz options
graphviz_output_format = "svg"

Now, there is a callgraph directive which has all the options of the graphviz directive and in addition:

Example to create a callgraph for the function pyan.create_callgraph that is zoomable, is defined from left to right and links each node to the API documentation that was created at the toctree path api.

.. callgraph:: pyan.create_callgraph
   :toctree: api
   :zoomable:
   :direction: horizontal

Troubleshooting

If GraphViz says _trouble in initrank, try adding -Gnewrank=true, as in:

dot -Gnewrank=true -Tsvg myuses.dot >myuses.svg

Usually either old or new rank (but often not both) works; this is a long-standing GraphViz issue with complex graphs.

Too much detail?

If the graph is visually unreadable due to too much detail, consider visualizing only a subset of the files in your project. Any references to files outside the analyzed set will be considered as undefined, and will not be drawn.

Currently Pyan always operates at the level of individual functions and methods; an option to visualize only relations between namespaces may (or may not) be added in a future version.

Features

Items tagged with ☆ are new in Pyan3.

Graph creation:

Analysis:

TODO

The analyzer does not currently support:

How it works

From the viewpoint of graphing the defines and uses relations, the interesting parts of the AST are bindings (defining new names, or assigning new values to existing names), and any name that appears in an ast.Load context (i.e. a use). The latter includes function calls; the function's name then appears in a load context inside the ast.Call node that represents the call site.

Bindings are tracked, with lexical scoping, to determine which type of object, or which function, each name points to at any given point in the source code being analyzed. This allows tracking things like:

def some_func():
    pass

class MyClass:
    def __init__(self):
        self.f = some_func

    def dostuff(self)
        self.f()

By tracking the name self.f, the analyzer will see that MyClass.dostuff() uses some_func().

The analyzer also needs to keep track of what type of object self currently points to. In a method definition, the literal name representing self is captured from the argument list, as Python does; then in the lexical scope of that method, that name points to the current class (since Pyan cares only about object types, not instances).

Of course, this simple approach cannot correctly track cases where the current binding of self.f depends on the order in which the methods of the class are executed. To keep things simple, Pyan decides to ignore this complication, just reads through the code in a linear fashion (twice so that any forward-references are picked up), and uses the most recent binding that is currently in scope.

When a binding statement is encountered, the current namespace determines in which scope to store the new value for the name. Similarly, when encountering a use, the current namespace determines which object type or function to tag as the user.

Authors

See AUTHORS.md.

License

GPL v2, as per comments here.