merged branch python3

This commit is contained in:
Tarek Ziade 2012-02-21 11:49:21 +01:00
commit 29bb206fd4
9 changed files with 153 additions and 48 deletions

View file

@ -1,3 +1,8 @@
lib lib
include include
.*\.pyc$ .*\.pyc$
dist
bin
flake8.egg-info
man
\.Python

View file

@ -3,9 +3,50 @@
http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html
MIT License. MIT License.
""" """
import compiler try:
from compiler import parse
iter_child_nodes = None
except ImportError:
from ast import parse, iter_child_nodes
import optparse import optparse
import sys import sys
from collections import defaultdict
class ASTVisitor:
VERBOSE = 0
def __init__(self):
self.node = None
self._cache = {}
def default(self, node, *args):
if hasattr(node, 'getChildNodes'):
children = node.getChildNodes()
else:
children = iter_child_nodes(node)
for child in children:
self.dispatch(child, *args)
def dispatch(self, node, *args):
self.node = node
klass = node.__class__
meth = self._cache.get(klass)
if meth is None:
className = klass.__name__
meth = getattr(self.visitor, 'visit' + className, self.default)
self._cache[klass] = meth
return meth(node, *args)
def preorder(self, tree, visitor, *args):
"""Do preorder walk of tree using visitor"""
self.visitor = visitor
visitor.visit = self.dispatch
self.dispatch(tree, *args) # XXX *args make sense?
class PathNode: class PathNode:
@ -14,8 +55,8 @@ class PathNode:
self.look = look self.look = look
def to_dot(self): def to_dot(self):
print 'node [shape=%s,label="%s"] %d;' % \ print('node [shape=%s,label="%s"] %d;' % \
(self.look, self.name, self.dot_id()) (self.look, self.name, self.dot_id()))
def dot_id(self): def dot_id(self):
return id(self) return id(self)
@ -24,25 +65,19 @@ class PathNode:
class PathGraph: class PathGraph:
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.nodes = {} self.nodes = defaultdict(list)
def add_node(self, n):
assert n
self.nodes.setdefault(n, [])
def connect(self, n1, n2): def connect(self, n1, n2):
assert n1 self.nodes[n1].append(n2)
assert n2
self.nodes.setdefault(n1, []).append(n2)
def to_dot(self): def to_dot(self):
print 'subgraph {' print('subgraph {')
for node in self.nodes: for node in self.nodes:
node.to_dot() node.to_dot()
for node, nexts in self.nodes.items(): for node, nexts in self.nodes.items():
for next in nexts: for next in nexts:
print '%s -- %s;' % (node.dot_id(), next.dot_id()) print('%s -- %s;' % (node.dot_id(), next.dot_id()))
print '}' print('}')
def complexity(self): def complexity(self):
""" Return the McCabe complexity for the graph. """ Return the McCabe complexity for the graph.
@ -53,13 +88,13 @@ class PathGraph:
return num_edges - num_nodes + 2 return num_edges - num_nodes + 2
class PathGraphingAstVisitor(compiler.visitor.ASTVisitor): class PathGraphingAstVisitor(ASTVisitor):
""" A visitor for a parsed Abstract Syntax Tree which finds executable """ A visitor for a parsed Abstract Syntax Tree which finds executable
statements. statements.
""" """
def __init__(self): def __init__(self):
compiler.visitor.ASTVisitor.__init__(self) ASTVisitor.__init__(self)
self.classname = "" self.classname = ""
self.graphs = {} self.graphs = {}
self.reset() self.reset()
@ -94,6 +129,8 @@ class PathGraphingAstVisitor(compiler.visitor.ASTVisitor):
self.graphs["%s%s" % (self.classname, node.name)] = self.graph self.graphs["%s%s" % (self.classname, node.name)] = self.graph
self.reset() self.reset()
visitFunctionDef = visitFunction
def visitClass(self, node): def visitClass(self, node):
old_classname = self.classname old_classname = self.classname
self.classname += node.name + "." self.classname += node.name + "."
@ -104,7 +141,6 @@ class PathGraphingAstVisitor(compiler.visitor.ASTVisitor):
if not self.tail: if not self.tail:
return return
pathnode = PathNode(name) pathnode = PathNode(name)
self.graph.add_node(pathnode)
self.graph.connect(self.tail, pathnode) self.graph.connect(self.tail, pathnode)
self.tail = pathnode self.tail = pathnode
return pathnode return pathnode
@ -171,13 +207,29 @@ class PathGraphingAstVisitor(compiler.visitor.ASTVisitor):
# TODO: visitTryFinally # TODO: visitTryFinally
# TODO: visitWith # TODO: visitWith
# XXX todo: determine which ones can add to the complexity
# py2
# TODO: visitStmt
# TODO: visitAssName
# TODO: visitCallFunc
# TODO: visitConst
# py3
# TODO: visitStore
# TODO: visitCall
# TODO: visitLoad
# TODO: visitNum
# TODO: visitarguments
# TODO: visitExpr
def get_code_complexity(code, min=7, filename='stdin'): def get_code_complexity(code, min=7, filename='stdin'):
complex = [] complex = []
try: try:
ast = compiler.parse(code) ast = parse(code)
except AttributeError as e: except AttributeError:
print >> sys.stderr, "Unable to parse %s: %s" % (filename, e) e = sys.exc_info()[1]
sys.stderr.write("Unable to parse %s: %s\n" % (filename, e))
return 0 return 0
visitor = PathGraphingAstVisitor() visitor = PathGraphingAstVisitor()
@ -215,20 +267,20 @@ def main(argv):
options, args = opar.parse_args(argv) options, args = opar.parse_args(argv)
text = open(args[0], "rU").read() + '\n\n' text = open(args[0], "rU").read() + '\n\n'
ast = compiler.parse(text) ast = parse(text)
visitor = PathGraphingAstVisitor() visitor = PathGraphingAstVisitor()
visitor.preorder(ast, visitor) visitor.preorder(ast, visitor)
if options.dot: if options.dot:
print 'graph {' print('graph {')
for graph in visitor.graphs.values(): for graph in visitor.graphs.values():
if graph.complexity() >= options.min: if graph.complexity() >= options.min:
graph.to_dot() graph.to_dot()
print '}' print('}')
else: else:
for graph in visitor.graphs.values(): for graph in visitor.graphs.values():
if graph.complexity() >= options.min: if graph.complexity() >= options.min:
print graph.name, graph.complexity() print(graph.name, graph.complexity())
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -93,7 +93,7 @@ for space.
""" """
from flake8 import __version__ as flake8_version from flake8 import __version__ as flake8_version
from pyflakes import __version__ as pep8_version from flake8.pyflakes import __version__ as pep8_version
__version__ = '0.6.1' __version__ = '0.6.1'

View file

@ -2,7 +2,11 @@
# (c) 2005-2010 Divmod, Inc. # (c) 2005-2010 Divmod, Inc.
# See LICENSE file for details # See LICENSE file for details
import __builtin__ try:
import __builtin__
except ImportError:
import builtins as __builtin__
import os.path import os.path
import _ast import _ast
import sys import sys
@ -251,7 +255,7 @@ class Checker(object):
all = [] all = []
# Look for imported names that aren't used. # Look for imported names that aren't used.
for importation in scope.itervalues(): for importation in scope.values():
if isinstance(importation, Importation): if isinstance(importation, Importation):
if not importation.used and importation.name not in all: if not importation.used and importation.name not in all:
self.report( self.report(
@ -284,7 +288,7 @@ class Checker(object):
def handleNode(self, node, parent): def handleNode(self, node, parent):
node.parent = parent node.parent = parent
if self.traceTree: if self.traceTree:
print ' ' * self.nodeDepth + node.__class__.__name__ print(' ' * self.nodeDepth + node.__class__.__name__)
self.nodeDepth += 1 self.nodeDepth += 1
if self.futuresAllowed and not \ if self.futuresAllowed and not \
(isinstance(node, _ast.ImportFrom) or self.isDocstring(node)): (isinstance(node, _ast.ImportFrom) or self.isDocstring(node)):
@ -296,7 +300,7 @@ class Checker(object):
finally: finally:
self.nodeDepth -= 1 self.nodeDepth -= 1
if self.traceTree: if self.traceTree:
print ' ' * self.nodeDepth + 'end ' + node.__class__.__name__ print(' ' * self.nodeDepth + 'end ' + node.__class__.__name__)
def ignore(self, node): def ignore(self, node):
pass pass
@ -325,7 +329,10 @@ class Checker(object):
EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore
# additional node types # additional node types
COMPREHENSION = EXCEPTHANDLER = KEYWORD = handleChildren COMPREHENSION = KEYWORD = handleChildren
def EXCEPTHANDLER(self, node):
self.scope[node.name] = node
def addBinding(self, lineno, value, reportRedef=True): def addBinding(self, lineno, value, reportRedef=True):
'''Called when a binding is altered. '''Called when a binding is altered.
@ -527,10 +534,15 @@ class Checker(object):
if isinstance(arg, _ast.Tuple): if isinstance(arg, _ast.Tuple):
addArgs(arg.elts) addArgs(arg.elts)
else: else:
if arg.id in args: try:
id_ = arg.id
except AttributeError:
id_ = arg.arg
if id_ in args:
self.report(messages.DuplicateArgument, self.report(messages.DuplicateArgument,
node.lineno, arg.id) node.lineno, id_)
args.append(arg.id) args.append(id_)
self.pushFunctionScope() self.pushFunctionScope()
addArgs(node.args.args) addArgs(node.args.args)
@ -554,7 +566,7 @@ class Checker(object):
""" """
Check to see if any assignments have not been used. Check to see if any assignments have not been used.
""" """
for name, binding in self.scope.iteritems(): for name, binding in self.scope.items():
if (not binding.used and not name in self.scope.globals if (not binding.used and not name in self.scope.globals
and isinstance(binding, Assignment)): and isinstance(binding, Assignment)):
self.report(messages.UnusedVariable, self.report(messages.UnusedVariable,
@ -629,13 +641,14 @@ def checkPath(filename):
@return: the number of warnings printed @return: the number of warnings printed
""" """
try: try:
return check(file(filename, 'U').read() + '\n', filename) return check(open(filename, 'U').read() + '\n', filename)
except IOError, msg: except IOError:
msg = sys.exc_info()[1]
print >> sys.stderr, "%s: %s" % (filename, msg.args[1]) print >> sys.stderr, "%s: %s" % (filename, msg.args[1])
return 1 return 1
def check(codeString, filename): def check(codeString, filename='(code)'):
""" """
Check the Python source given by C{codeString} for flakes. Check the Python source given by C{codeString} for flakes.
@ -652,7 +665,8 @@ def check(codeString, filename):
# First, compile into an AST and handle syntax errors. # First, compile into an AST and handle syntax errors.
try: try:
tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST)
except SyntaxError, value: except SyntaxError:
value = sys.exc_info()[1]
msg = value.args[0] msg = value.args[0]
(lineno, offset, text) = value.lineno, value.offset, value.text (lineno, offset, text) = value.lineno, value.offset, value.text
@ -679,13 +693,15 @@ def check(codeString, filename):
else: else:
# Okay, it's syntactically valid. Now check it. # Okay, it's syntactically valid. Now check it.
w = Checker(tree, filename) w = Checker(tree, filename)
w.messages.sort(lambda a, b: cmp(a.lineno, b.lineno)) sorting = [(msg.lineno, msg) for msg in w.messages]
sorting.sort()
w.messages = [msg for index, msg in sorting]
valid_warnings = 0 valid_warnings = 0
for warning in w.messages: for warning in w.messages:
if skip_warning(warning): if skip_warning(warning):
continue continue
print warning print(warning)
valid_warnings += 1 valid_warnings += 1
return valid_warnings return valid_warnings

View file

@ -60,7 +60,7 @@ def main():
def _get_files(repo, **kwargs): def _get_files(repo, **kwargs):
seen = set() seen = set()
for rev in xrange(repo[kwargs['node']], len(repo)): for rev in range(repo[kwargs['node']], len(repo)):
for file_ in repo[rev].files(): for file_ in repo[rev].files():
file_ = os.path.join(repo.root, file_) file_ = os.path.join(repo.root, file_)
if file_ in seen or not os.path.exists(file_): if file_ in seen or not os.path.exists(file_):
@ -118,6 +118,8 @@ def git_hook(complexity=-1, strict=False):
ext = os.path.splitext(filename)[-1] ext = os.path.splitext(filename)[-1]
if ext != '.py': if ext != '.py':
continue continue
if not os.path.exists(filename):
continue
warnings += check_file(filename, complexity) warnings += check_file(filename, complexity)
if strict: if strict:

View file

@ -0,0 +1,17 @@
from unittest import TestCase
from flake8.pyflakes import check
code = """
try:
pass
except ValueError as err:
print(err)
"""
class TestFlake(TestCase):
def test_exception(self):
warnings = check(code)
self.assertEqual(warnings, 0)

View file

@ -1,6 +1,9 @@
import unittest import unittest
import sys import sys
from StringIO import StringIO try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from flake8.mccabe import get_code_complexity from flake8.mccabe import get_code_complexity
@ -33,6 +36,6 @@ class McCabeTest(unittest.TestCase):
self.assertEqual(get_code_complexity(_GLOBAL, 1), 2) self.assertEqual(get_code_complexity(_GLOBAL, 1), 2)
self.out.seek(0) self.out.seek(0)
res = self.out.read().strip().split('\n') res = self.out.read().strip().split('\n')
wanted = ["stdin:5:1: 'a' is too complex (3)", wanted = ["stdin:5:1: 'a' is too complex (4)",
'stdin:Loop 2 is too complex (1)'] 'stdin:Loop 2 is too complex (2)']
self.assertEqual(res, wanted) self.assertEqual(res, wanted)

View file

@ -1,8 +1,11 @@
import re import re
import os
def skip_warning(warning): def skip_warning(warning):
# XXX quick dirty hack, just need to keep the line in the warning # XXX quick dirty hack, just need to keep the line in the warning
if not os.path.isfile(warning.filename):
return False
line = open(warning.filename).readlines()[warning.lineno - 1] line = open(warning.filename).readlines()[warning.lineno - 1]
return skip_line(line) return skip_line(line)

View file

@ -1,7 +1,14 @@
try: import sys
from setuptools import setup
except ImportError: ispy3 = sys.version_info[0] == 3
from distutils.core import setup # NOQA
if ispy3:
from distutils.core import setup
else:
try:
from setuptools import setup # NOQA
except ImportError:
from distutils.core import setup # NOQA
from flake8 import __version__ from flake8 import __version__