Commits

Tarek Ziadé committed ed98227

added the mccabe metric

Comments (0)

Files changed (3)

flake8/__init__.py

 """
 Implementation of the command-line I{flake8} tool.
 """
-import re
 import sys
 import os
 import _ast
 import pep8
+import mccabe
+import re
+
 
 checker = __import__('flake8.checker').checker
 
 
 def _noqa(warning):
     # XXX quick dirty hack, just need to keep the line in the warning
-    line = open(warning.filename).readlines()[warning.lineno - 1]
+    line = open(warning.filename).readlines()[warning.lineno-1]
     return line.strip().lower().endswith('# noqa')
 
 
-_NOQA = re.compile(r'^# flake8: noqa', re.I | re.M)
-
-
-def skip_file(path):
-    """Returns True if this header is found in path
-
-    # -*- flake8: noqa -*-
-    """
-    f = open(path)
-    try:
-        content = f.read()
-    finally:
-        f.close()
-    return _NOQA.match(content) is not None
-
-
 def checkPath(filename):
     """
     Check the given path, printing out any warnings detected.
         return 1
 
 
+def check_file(path, complexity=10):
+    warnings = checkPath(path)
+    warnings += pep8.input_file(path)
+    warnings += mccabe.get_module_complexity(path, complexity)
+    return warnings
+
+
+def check_code(code, complexity=10):
+    warnings = check(code, '<stdin>')
+    warnings += mccabe.get_code_complexity(code, complexity)
+    return warnings
+
+
+_NOQA = re.compile(r'^# flake8: noqa', re.I | re.M)
+
+
+def skip_file(path):
+    """Returns True if this header is found in path
+
+    # flake8: noqa
+    """
+    f = open(path)
+    try:
+        content = f.read()
+    finally:
+        f.close()
+    return _NOQA.match(content) is not None
+
+
+def _get_python_files(paths):
+    for path in paths:
+        if os.path.isdir(path):
+            for dirpath, dirnames, filenames in os.walk(path):
+                for filename in filenames:
+                    if not filename.endswith('.py'):
+                        continue
+                    fullpath = os.path.join(dirpath, filename)
+                    if not skip_file(fullpath):
+                        yield fullpath
+
+        else:
+            if not skip_file(path):
+                yield path
+
+
 def main():
     pep8.process_options()
-
     warnings = 0
     args = sys.argv[1:]
     if args:
-        for arg in args:
-            if os.path.isdir(arg):
-                for dirpath, dirnames, filenames in os.walk(arg):
-                    for filename in filenames:
-                        if not filename.endswith('.py'):
-                            continue
-                        fullpath = os.path.join(dirpath, filename)
-                        if skip_file(fullpath):
-                            continue
-                        warnings += checkPath(fullpath)
-                        warnings += pep8.input_file(fullpath)
-            else:
-                if skip_file(arg):
-                    continue
-                warnings += checkPath(arg)
-                warnings += pep8.input_file(arg)
-
+        for path in _get_python_files(args):
+            warnings += check_file(path)
     else:
         stdin = sys.stdin.read()
-        warnings += check(stdin, '<stdin>')
+        warnings += check_code(stdin)
 
     raise SystemExit(warnings > 0)
 
 
-def hg_hook(ui, repo, **kwargs):
-    pep8.process_options()
-    warnings = 0
-    files = []
+def _get_files(repo, **kwargs):
     for rev in xrange(repo[kwargs['node']], len(repo)):
         for file_ in repo[rev].files():
             if not file_.endswith('.py'):
                 continue
             if skip_file(file_):
                 continue
-            if file_ not in files:
-                files.append(file_)
+            yield file_
 
-    for file_ in files:
-        warnings += checkPath(file_)
-        warnings += pep8.input_file(file_)
+
+def hg_hook(ui, repo, **kwargs):
+    pep8.process_options()
+    warnings = 0
+    for file_ in _get_files(repo, **kwargs):
+        warnings += check_file(file_)
 
     strict = ui.config('flake8', 'strict')
     if strict is None:
         return warnings
 
     return 0
+
+if __name__ == '__main__':
+    main()
+""" Meager code path measurement tool.
+    Ned Batchelder
+    http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
+    MIT License.
+"""
+
+import compiler, optparse, sys
+
+
+class PathNode:
+    def __init__(self, name, look="circle"):
+        self.name = name
+        self.look = look
+
+    def to_dot(self):
+        print 'node [shape=%s,label="%s"] %d;' % (self.look, self.name, self.dot_id())
+
+    def dot_id(self):
+        return id(self)
+
+
+class PathGraph:
+    def __init__(self, name):
+        self.name = name
+        self.nodes = {}
+
+    def add_node(self, n):
+        assert n
+        self.nodes.setdefault(n, [])
+
+    def connect(self, n1, n2):
+        assert n1
+        assert n2
+        self.nodes.setdefault(n1, []).append(n2)
+
+    def to_dot(self):
+        print 'subgraph {'
+        for node in self.nodes:
+            node.to_dot()
+        for node, nexts in self.nodes.items():
+            for next in nexts:
+                print '%s -- %s;' % (node.dot_id(), next.dot_id())
+        print '}'
+
+    def complexity(self):
+        """ Return the McCabe complexity for the graph.
+            V-E+2
+        """
+        num_edges = sum([len(n) for n in self.nodes.values()])
+        num_nodes = len(self.nodes)
+        return num_edges - num_nodes + 2
+
+
+class PathGraphingAstVisitor(compiler.visitor.ASTVisitor):
+    """ A visitor for a parsed Abstract Syntax Tree which finds executable
+        statements.
+    """
+
+    def __init__(self):
+        compiler.visitor.ASTVisitor.__init__(self)
+        self.classname = ""
+        self.graphs = {}
+        self.reset()
+
+    def reset(self):
+        self.graph = None
+        self.tail = None
+
+    def visitFunction(self, node):
+        if self.classname:
+            entity = '%s%s' % (self.classname, node.name)
+        else:
+            entity = node.name
+
+        name = '%d:1: %r' % (node.lineno, entity)
+        self.graph = PathGraph(name)
+        pathnode = PathNode(name)
+        self.tail = pathnode
+        self.default(node)
+        self.graphs["%s%s" % (self.classname, node.name)] = self.graph
+        self.reset()
+
+    def visitClass(self, node):
+        old_classname = self.classname
+        self.classname += node.name + "."
+        self.default(node)
+        self.classname = old_classname
+
+    def appendPathNode(self, name):
+        if not self.tail:
+            return
+        pathnode = PathNode(name)
+        self.graph.add_node(pathnode)
+        self.graph.connect(self.tail, pathnode)
+        self.tail = pathnode
+        return pathnode
+
+    def visitSimpleStatement(self, node):
+        name = "Stmt %d" % node.lineno
+        self.appendPathNode(name)
+
+    visitAssert = visitAssign = visitAssTuple = visitPrint = \
+        visitPrintnl = visitRaise = visitSubscript = visitDecorators = \
+        visitPass = visitDiscard = visitGlobal = visitReturn = \
+        visitSimpleStatement
+
+    def visitLoop(self, node):
+        name = "Loop %d" % node.lineno
+        pathnode = self.appendPathNode(name)
+        self.tail = pathnode
+        self.default(node.body)
+        bottom = PathNode("", look='point')
+        self.graph.connect(self.tail, bottom)
+        self.graph.connect(pathnode, bottom)
+        self.tail = bottom
+        # TODO: else clause in node.else_
+
+    visitFor = visitWhile = visitLoop
+
+    def visitIf(self, node):
+        name = "If %d" % node.lineno
+        pathnode = self.appendPathNode(name)
+        if not pathnode:
+            return  # TODO: figure out what to do with if's outside def's.
+        loose_ends = []
+        for t, n in node.tests:
+            self.tail = pathnode
+            self.default(n)
+            loose_ends.append(self.tail)
+        if node.else_:
+            self.tail = pathnode
+            self.default(node.else_)
+            loose_ends.append(self.tail)
+        else:
+            loose_ends.append(pathnode)
+        bottom = PathNode("", look='point')
+        for le in loose_ends:
+            self.graph.connect(le, bottom)
+        self.tail = bottom
+
+    # TODO: visitTryExcept
+    # TODO: visitTryFinally
+    # TODO: visitWith
+
+
+def get_code_complexity(code, min=7, filename='stdin'):
+    complex = []
+    ast = compiler.parse(code)
+    visitor = PathGraphingAstVisitor()
+    try:
+        visitor.preorder(ast, visitor)
+    except AttributeError:
+        print('McCabe: Could not parse code')
+        return -1
+
+    for graph in visitor.graphs.values():
+        if graph is None:
+            # ?
+            continue
+        if graph.complexity() >= min:
+            msg = '%s:%s is too complex (%d)' % (filename,
+                    graph.name, graph.complexity())
+            complex.append(msg)
+
+    if len(complex) == 0:
+        return 0
+
+    print('\n'.join(complex))
+    return len(complex)
+
+
+def get_module_complexity(module_path, min=7):
+    """Returns the complexity of a module"""
+    code = open(module_path, "rU").read() + '\n\n'
+    res = get_code_complexity(code, min, filename=module_path)
+    if res == -1:
+        print('McCabe: Problem with %s' % module_path)
+    return res
+
+
+def main(argv):
+    opar = optparse.OptionParser()
+    opar.add_option("-d", "--dot", dest="dot", help="output a graphviz dot file", action="store_true")
+    opar.add_option("-m", "--min", dest="min", help="minimum complexity for output", type="int", default=2)
+
+    options, args = opar.parse_args(argv)
+
+    text = open(args[0], "rU").read()+'\n\n'
+    ast = compiler.parse(text)
+    visitor = PathGraphingAstVisitor()
+    visitor.preorder(ast, visitor)
+
+    if options.dot:
+        print 'graph {'
+        for graph in visitor.graphs.values():
+            if graph.complexity() >= options.min:
+                graph.to_dot()
+        print '}'
+    else:
+        for graph in visitor.graphs.values():
+            if graph.complexity() >= options.min:
+                print graph.name, graph.complexity()
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
 #!/usr/bin/python
+# flake8: noqa
 # pep8.py - Check Python source code formatting, according to PEP 8
 # Copyright (C) 2006 Johann C. Rocholl <johann@rocholl.net>
 #