diff --git a/.hgignore b/.hgignore index 6294639..239c799 100644 --- a/.hgignore +++ b/.hgignore @@ -1,3 +1,8 @@ lib include .*\.pyc$ +dist +bin +flake8.egg-info +man +\.Python diff --git a/flake8/mccabe.py b/flake8/mccabe.py index b693ca7..ee89ed0 100644 --- a/flake8/mccabe.py +++ b/flake8/mccabe.py @@ -3,9 +3,50 @@ http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html 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 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: @@ -14,8 +55,8 @@ class PathNode: self.look = look def to_dot(self): - print 'node [shape=%s,label="%s"] %d;' % \ - (self.look, self.name, self.dot_id()) + print('node [shape=%s,label="%s"] %d;' % \ + (self.look, self.name, self.dot_id())) def dot_id(self): return id(self) @@ -24,25 +65,19 @@ class PathNode: class PathGraph: def __init__(self, name): self.name = name - self.nodes = {} - - def add_node(self, n): - assert n - self.nodes.setdefault(n, []) + self.nodes = defaultdict(list) def connect(self, n1, n2): - assert n1 - assert n2 - self.nodes.setdefault(n1, []).append(n2) + self.nodes[n1].append(n2) def to_dot(self): - print 'subgraph {' + 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 '}' + print('%s -- %s;' % (node.dot_id(), next.dot_id())) + print('}') def complexity(self): """ Return the McCabe complexity for the graph. @@ -53,13 +88,13 @@ class PathGraph: 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 statements. """ def __init__(self): - compiler.visitor.ASTVisitor.__init__(self) + ASTVisitor.__init__(self) self.classname = "" self.graphs = {} self.reset() @@ -94,6 +129,8 @@ class PathGraphingAstVisitor(compiler.visitor.ASTVisitor): self.graphs["%s%s" % (self.classname, node.name)] = self.graph self.reset() + visitFunctionDef = visitFunction + def visitClass(self, node): old_classname = self.classname self.classname += node.name + "." @@ -104,7 +141,6 @@ class PathGraphingAstVisitor(compiler.visitor.ASTVisitor): if not self.tail: return pathnode = PathNode(name) - self.graph.add_node(pathnode) self.graph.connect(self.tail, pathnode) self.tail = pathnode return pathnode @@ -171,13 +207,29 @@ class PathGraphingAstVisitor(compiler.visitor.ASTVisitor): # TODO: visitTryFinally # 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'): complex = [] try: - ast = compiler.parse(code) - except AttributeError as e: - print >> sys.stderr, "Unable to parse %s: %s" % (filename, e) + ast = parse(code) + except AttributeError: + e = sys.exc_info()[1] + sys.stderr.write("Unable to parse %s: %s\n" % (filename, e)) return 0 visitor = PathGraphingAstVisitor() @@ -215,20 +267,20 @@ def main(argv): options, args = opar.parse_args(argv) text = open(args[0], "rU").read() + '\n\n' - ast = compiler.parse(text) + ast = parse(text) visitor = PathGraphingAstVisitor() visitor.preorder(ast, visitor) if options.dot: - print 'graph {' + print('graph {') for graph in visitor.graphs.values(): if graph.complexity() >= options.min: graph.to_dot() - print '}' + print('}') else: for graph in visitor.graphs.values(): if graph.complexity() >= options.min: - print graph.name, graph.complexity() + print(graph.name, graph.complexity()) if __name__ == '__main__': diff --git a/flake8/pep8.py b/flake8/pep8.py index a3e93cd..bdde37b 100644 --- a/flake8/pep8.py +++ b/flake8/pep8.py @@ -93,7 +93,7 @@ for space. """ 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' diff --git a/flake8/pyflakes.py b/flake8/pyflakes.py index 036b25c..c83d210 100644 --- a/flake8/pyflakes.py +++ b/flake8/pyflakes.py @@ -2,7 +2,11 @@ # (c) 2005-2010 Divmod, Inc. # See LICENSE file for details -import __builtin__ +try: + import __builtin__ +except ImportError: + import builtins as __builtin__ + import os.path import _ast import sys @@ -251,7 +255,7 @@ class Checker(object): all = [] # Look for imported names that aren't used. - for importation in scope.itervalues(): + for importation in scope.values(): if isinstance(importation, Importation): if not importation.used and importation.name not in all: self.report( @@ -284,7 +288,7 @@ class Checker(object): def handleNode(self, node, parent): node.parent = parent if self.traceTree: - print ' ' * self.nodeDepth + node.__class__.__name__ + print(' ' * self.nodeDepth + node.__class__.__name__) self.nodeDepth += 1 if self.futuresAllowed and not \ (isinstance(node, _ast.ImportFrom) or self.isDocstring(node)): @@ -296,7 +300,7 @@ class Checker(object): finally: self.nodeDepth -= 1 if self.traceTree: - print ' ' * self.nodeDepth + 'end ' + node.__class__.__name__ + print(' ' * self.nodeDepth + 'end ' + node.__class__.__name__) def ignore(self, node): pass @@ -325,7 +329,10 @@ class Checker(object): EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore # 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): '''Called when a binding is altered. @@ -527,10 +534,15 @@ class Checker(object): if isinstance(arg, _ast.Tuple): addArgs(arg.elts) else: - if arg.id in args: + try: + id_ = arg.id + except AttributeError: + id_ = arg.arg + + if id_ in args: self.report(messages.DuplicateArgument, - node.lineno, arg.id) - args.append(arg.id) + node.lineno, id_) + args.append(id_) self.pushFunctionScope() addArgs(node.args.args) @@ -554,7 +566,7 @@ class Checker(object): """ 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 and isinstance(binding, Assignment)): self.report(messages.UnusedVariable, @@ -629,13 +641,14 @@ def checkPath(filename): @return: the number of warnings printed """ try: - return check(file(filename, 'U').read() + '\n', filename) - except IOError, msg: + return check(open(filename, 'U').read() + '\n', filename) + except IOError: + msg = sys.exc_info()[1] print >> sys.stderr, "%s: %s" % (filename, msg.args[1]) return 1 -def check(codeString, filename): +def check(codeString, filename='(code)'): """ 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. try: tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) - except SyntaxError, value: + except SyntaxError: + value = sys.exc_info()[1] msg = value.args[0] (lineno, offset, text) = value.lineno, value.offset, value.text @@ -679,13 +693,15 @@ def check(codeString, filename): else: # Okay, it's syntactically valid. Now check it. 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 for warning in w.messages: if skip_warning(warning): continue - print warning + print(warning) valid_warnings += 1 return valid_warnings diff --git a/flake8/run.py b/flake8/run.py index a24807b..4a7ddfa 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -60,7 +60,7 @@ def main(): def _get_files(repo, **kwargs): 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(): file_ = os.path.join(repo.root, 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] if ext != '.py': continue + if not os.path.exists(filename): + continue warnings += check_file(filename, complexity) if strict: diff --git a/flake8/tests/test_flakes.py b/flake8/tests/test_flakes.py new file mode 100644 index 0000000..815e61e --- /dev/null +++ b/flake8/tests/test_flakes.py @@ -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) diff --git a/flake8/tests/test_mccabe.py b/flake8/tests/test_mccabe.py index f1ae82b..768accd 100644 --- a/flake8/tests/test_mccabe.py +++ b/flake8/tests/test_mccabe.py @@ -1,6 +1,9 @@ import unittest import sys -from StringIO import StringIO +try: + from StringIO import StringIO +except ImportError: + from io import StringIO from flake8.mccabe import get_code_complexity @@ -33,6 +36,6 @@ class McCabeTest(unittest.TestCase): self.assertEqual(get_code_complexity(_GLOBAL, 1), 2) self.out.seek(0) res = self.out.read().strip().split('\n') - wanted = ["stdin:5:1: 'a' is too complex (3)", - 'stdin:Loop 2 is too complex (1)'] + wanted = ["stdin:5:1: 'a' is too complex (4)", + 'stdin:Loop 2 is too complex (2)'] self.assertEqual(res, wanted) diff --git a/flake8/util.py b/flake8/util.py index 89640bb..2bcfa66 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -1,8 +1,11 @@ import re +import os def skip_warning(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] return skip_line(line) diff --git a/setup.py b/setup.py index 49d6ef7..4c036bf 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,14 @@ -try: - from setuptools import setup -except ImportError: - from distutils.core import setup # NOQA +import sys + +ispy3 = sys.version_info[0] == 3 + +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__