Search code examples
pythonabstract-syntax-tree

Find number of lines in python functions without running it


I am trying to create a tool that identifies the number of lines of code in each function in a given python file. However- I do not want to actually run the code which is required with something like the inspect library suggested in other posts.

Is there a pre-existing tool I can use to get the source code for each function, or even a given function within the plain text of a .py file? Or will I need to create some custom regex/code for this?

Edit : Want to clarify since people are suggesting a few things I have already tried. AST provides me the ability to find the function name- but not the number of lines of the function, nor the source for the function or anything else that would lead me to it (from what I can see). Inspect can do it, but requires running the code (unless I am misunderstanding something).


Solution

  • You can use the ast module to walk through the parse tree of a Python program. This requires that the program not have any syntax errors, but it will not attempt to execute the program, so runtime errors won't hinder the process. (In particular, since it doesn't execute import statements, it won't descend into imported modules.)

    The following very simple sample assumes that:

    • By "lines", you mean the physical lines, as opposed to logical lines. (This code prints the start and end line numbers, but you could change that to node.end_lineno - node.lineno + 1 if you just want the line count.)
    • You want to list each function, including nested functions and class members. This code tracks (and prints) class blocks as well as def blocks, indenting nested objects by four spaces (or the value of the indent argument).
    • You don't care about async defs. The only reason I left them out was to save space; there's really nothing special about them, and you would just have to add one more visit method to capture them.
    import ast
    class ListVisitor(ast.NodeVisitor):
        def __init__(self, indent=4):
            self.cur = 0
            self.ind = indent
        def visit_FunctionDef(self, node):
            print(f"{'':>{self.cur}}Function {node.name} {node.lineno}->{node.end_lineno}")
            self.cur += self.ind
            self.generic_visit(node)
            self.cur -= self.ind
        def visit_ClassDef(self, node):
            print(f"{'':>{self.cur}}Class {node.name} {node.lineno}->{node.end_lineno}")
            self.cur += self.ind
            self.generic_visit(node)
            self.cur -= self.ind
    
    if __name__ == "__main__":
        from sys import argv
        v = ListVisitor()
        for fn in argv[1:]:
            try:
                with open(fn) as source:
                    v.visit(ast.parse(source.read(), filename=fn, mode='exec'))
            except SyntaxError as e:
                print(e)
            except OSError as e:
                print(e)
    

    Here's what the output looks like when I call it on itself:

    $ python3.9 -m listfuncs listfuncs.py
    Class ListVisitor 2->15
        Function __init__ 3->5
        Function visit_FunctionDef 6->10
        Function visit_ClassDef 11->15