commit 0da965fa7284090f4f413c41247b3730528edd88 Author: Tarek Ziade Date: Thu Aug 12 14:40:53 2010 +0200 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..42b8cf3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2005 Divmod, Inc., http://www.divmod.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bin/flake8 b/bin/flake8 new file mode 100755 index 0000000..0732e9a --- /dev/null +++ b/bin/flake8 @@ -0,0 +1,5 @@ +#!/usr/bin/python +from flake8.scripts.flake8 import main + +if __name__ == '__main__': + main() diff --git a/flake8/__init__.py b/flake8/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/flake8/__init__.py @@ -0,0 +1 @@ +# diff --git a/flake8/checker.py b/flake8/checker.py new file mode 100644 index 0000000..6d711f1 --- /dev/null +++ b/flake8/checker.py @@ -0,0 +1,624 @@ +# -*- test-case-name: pyflakes -*- +# (c) 2005-2010 Divmod, Inc. +# See LICENSE file for details + +import __builtin__ +import os.path +import _ast + +from pyflakes import messages + + +# utility function to iterate over an AST node's children, adapted +# from Python 2.6's standard ast module +try: + import ast + iter_child_nodes = ast.iter_child_nodes +except (ImportError, AttributeError): + def iter_child_nodes(node, astcls=_ast.AST): + """ + Yield all direct child nodes of *node*, that is, all fields that are nodes + and all items of fields that are lists of nodes. + """ + for name in node._fields: + field = getattr(node, name, None) + if isinstance(field, astcls): + yield field + elif isinstance(field, list): + for item in field: + yield item + + +class Binding(object): + """ + Represents the binding of a value to a name. + + The checker uses this to keep track of which names have been bound and + which names have not. See L{Assignment} for a special type of binding that + is checked with stricter rules. + + @ivar used: pair of (L{Scope}, line-number) indicating the scope and + line number that this binding was last used + """ + + def __init__(self, name, source): + self.name = name + self.source = source + self.used = False + + + def __str__(self): + return self.name + + + def __repr__(self): + return '<%s object %r from line %r at 0x%x>' % (self.__class__.__name__, + self.name, + self.source.lineno, + id(self)) + + + +class UnBinding(Binding): + '''Created by the 'del' operator.''' + + + +class Importation(Binding): + """ + A binding created by an import statement. + + @ivar fullName: The complete name given to the import statement, + possibly including multiple dotted components. + @type fullName: C{str} + """ + def __init__(self, name, source): + self.fullName = name + name = name.split('.')[0] + super(Importation, self).__init__(name, source) + + + +class Argument(Binding): + """ + Represents binding a name as an argument. + """ + + + +class Assignment(Binding): + """ + Represents binding a name with an explicit assignment. + + The checker will raise warnings for any Assignment that isn't used. Also, + the checker does not consider assignments in tuple/list unpacking to be + Assignments, rather it treats them as simple Bindings. + """ + + + +class FunctionDefinition(Binding): + pass + + + +class ExportBinding(Binding): + """ + A binding created by an C{__all__} assignment. If the names in the list + can be determined statically, they will be treated as names for export and + additional checking applied to them. + + The only C{__all__} assignment that can be recognized is one which takes + the value of a literal list containing literal strings. For example:: + + __all__ = ["foo", "bar"] + + Names which are imported and not otherwise used but appear in the value of + C{__all__} will not have an unused import warning reported for them. + """ + def names(self): + """ + Return a list of the names referenced by this binding. + """ + names = [] + if isinstance(self.source, _ast.List): + for node in self.source.elts: + if isinstance(node, _ast.Str): + names.append(node.s) + return names + + + +class Scope(dict): + importStarred = False # set to True when import * is found + + + def __repr__(self): + return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), dict.__repr__(self)) + + + def __init__(self): + super(Scope, self).__init__() + + + +class ClassScope(Scope): + pass + + + +class FunctionScope(Scope): + """ + I represent a name scope for a function. + + @ivar globals: Names declared 'global' in this function. + """ + def __init__(self): + super(FunctionScope, self).__init__() + self.globals = {} + + + +class ModuleScope(Scope): + pass + + +# Globally defined names which are not attributes of the __builtin__ module. +_MAGIC_GLOBALS = ['__file__', '__builtins__'] + + + +class Checker(object): + """ + I check the cleanliness and sanity of Python code. + + @ivar _deferredFunctions: Tracking list used by L{deferFunction}. Elements + of the list are two-tuples. The first element is the callable passed + to L{deferFunction}. The second element is a copy of the scope stack + at the time L{deferFunction} was called. + + @ivar _deferredAssignments: Similar to C{_deferredFunctions}, but for + callables which are deferred assignment checks. + """ + + nodeDepth = 0 + traceTree = False + + def __init__(self, tree, filename='(none)'): + self._deferredFunctions = [] + self._deferredAssignments = [] + self.dead_scopes = [] + self.messages = [] + self.filename = filename + self.scopeStack = [ModuleScope()] + self.futuresAllowed = True + self.handleChildren(tree) + self._runDeferred(self._deferredFunctions) + # Set _deferredFunctions to None so that deferFunction will fail + # noisily if called after we've run through the deferred functions. + self._deferredFunctions = None + self._runDeferred(self._deferredAssignments) + # Set _deferredAssignments to None so that deferAssignment will fail + # noisly if called after we've run through the deferred assignments. + self._deferredAssignments = None + del self.scopeStack[1:] + self.popScope() + self.check_dead_scopes() + + + def deferFunction(self, callable): + ''' + Schedule a function handler to be called just before completion. + + This is used for handling function bodies, which must be deferred + because code later in the file might modify the global scope. When + `callable` is called, the scope at the time this is called will be + restored, however it will contain any new bindings added to it. + ''' + self._deferredFunctions.append((callable, self.scopeStack[:])) + + + def deferAssignment(self, callable): + """ + Schedule an assignment handler to be called just after deferred + function handlers. + """ + self._deferredAssignments.append((callable, self.scopeStack[:])) + + + def _runDeferred(self, deferred): + """ + Run the callables in C{deferred} using their associated scope stack. + """ + for handler, scope in deferred: + self.scopeStack = scope + handler() + + + def scope(self): + return self.scopeStack[-1] + scope = property(scope) + + def popScope(self): + self.dead_scopes.append(self.scopeStack.pop()) + + + def check_dead_scopes(self): + """ + Look at scopes which have been fully examined and report names in them + which were imported but unused. + """ + for scope in self.dead_scopes: + export = isinstance(scope.get('__all__'), ExportBinding) + if export: + all = scope['__all__'].names() + if os.path.split(self.filename)[1] != '__init__.py': + # Look for possible mistakes in the export list + undefined = set(all) - set(scope) + for name in undefined: + self.report( + messages.UndefinedExport, + scope['__all__'].source.lineno, + name) + else: + all = [] + + # Look for imported names that aren't used. + for importation in scope.itervalues(): + if isinstance(importation, Importation): + if not importation.used and importation.name not in all: + self.report( + messages.UnusedImport, + importation.source.lineno, + importation.name) + + + def pushFunctionScope(self): + self.scopeStack.append(FunctionScope()) + + def pushClassScope(self): + self.scopeStack.append(ClassScope()) + + def report(self, messageClass, *args, **kwargs): + self.messages.append(messageClass(self.filename, *args, **kwargs)) + + def handleChildren(self, tree): + for node in iter_child_nodes(tree): + self.handleNode(node, tree) + + def isDocstring(self, node): + """ + Determine if the given node is a docstring, as long as it is at the + correct place in the node tree. + """ + return isinstance(node, _ast.Str) or \ + (isinstance(node, _ast.Expr) and + isinstance(node.value, _ast.Str)) + + def handleNode(self, node, parent): + node.parent = parent + if self.traceTree: + print ' ' * self.nodeDepth + node.__class__.__name__ + self.nodeDepth += 1 + if self.futuresAllowed and not \ + (isinstance(node, _ast.ImportFrom) or self.isDocstring(node)): + self.futuresAllowed = False + nodeType = node.__class__.__name__.upper() + try: + handler = getattr(self, nodeType) + handler(node) + finally: + self.nodeDepth -= 1 + if self.traceTree: + print ' ' * self.nodeDepth + 'end ' + node.__class__.__name__ + + def ignore(self, node): + pass + + # "stmt" type nodes + RETURN = DELETE = PRINT = WHILE = IF = WITH = RAISE = TRYEXCEPT = \ + TRYFINALLY = ASSERT = EXEC = EXPR = handleChildren + + CONTINUE = BREAK = PASS = ignore + + # "expr" type nodes + BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = YIELD = COMPARE = \ + CALL = REPR = ATTRIBUTE = SUBSCRIPT = LIST = TUPLE = handleChildren + + NUM = STR = ELLIPSIS = ignore + + # "slice" type nodes + SLICE = EXTSLICE = INDEX = handleChildren + + # expression contexts are node instances too, though being constants + LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore + + # same for operators + AND = OR = ADD = SUB = MULT = DIV = MOD = POW = LSHIFT = RSHIFT = \ + BITOR = BITXOR = BITAND = FLOORDIV = INVERT = NOT = UADD = USUB = \ + EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore + + # additional node types + COMPREHENSION = EXCEPTHANDLER = KEYWORD = handleChildren + + def addBinding(self, lineno, value, reportRedef=True): + '''Called when a binding is altered. + + - `lineno` is the line of the statement responsible for the change + - `value` is the optional new value, a Binding instance, associated + with the binding; if None, the binding is deleted if it exists. + - if `reportRedef` is True (default), rebinding while unused will be + reported. + ''' + if (isinstance(self.scope.get(value.name), FunctionDefinition) + and isinstance(value, FunctionDefinition)): + self.report(messages.RedefinedFunction, + lineno, value.name, self.scope[value.name].source.lineno) + + if not isinstance(self.scope, ClassScope): + for scope in self.scopeStack[::-1]: + existing = scope.get(value.name) + if (isinstance(existing, Importation) + and not existing.used + and (not isinstance(value, Importation) or value.fullName == existing.fullName) + and reportRedef): + + self.report(messages.RedefinedWhileUnused, + lineno, value.name, scope[value.name].source.lineno) + + if isinstance(value, UnBinding): + try: + del self.scope[value.name] + except KeyError: + self.report(messages.UndefinedName, lineno, value.name) + else: + self.scope[value.name] = value + + def GLOBAL(self, node): + """ + Keep track of globals declarations. + """ + if isinstance(self.scope, FunctionScope): + self.scope.globals.update(dict.fromkeys(node.names)) + + def LISTCOMP(self, node): + # handle generators before element + for gen in node.generators: + self.handleNode(gen, node) + self.handleNode(node.elt, node) + + GENERATOREXP = SETCOMP = LISTCOMP + + # dictionary comprehensions; introduced in Python 2.7 + def DICTCOMP(self, node): + for gen in node.generators: + self.handleNode(gen, node) + self.handleNode(node.key, node) + self.handleNode(node.value, node) + + def FOR(self, node): + """ + Process bindings for loop variables. + """ + vars = [] + def collectLoopVars(n): + if isinstance(n, _ast.Name): + vars.append(n.id) + elif isinstance(n, _ast.expr_context): + return + else: + for c in iter_child_nodes(n): + collectLoopVars(c) + + collectLoopVars(node.target) + for varn in vars: + if (isinstance(self.scope.get(varn), Importation) + # unused ones will get an unused import warning + and self.scope[varn].used): + self.report(messages.ImportShadowedByLoopVar, + node.lineno, varn, self.scope[varn].source.lineno) + + self.handleChildren(node) + + def NAME(self, node): + """ + Handle occurrence of Name (which can be a load/store/delete access.) + """ + # Locate the name in locals / function / globals scopes. + if isinstance(node.ctx, (_ast.Load, _ast.AugLoad)): + # try local scope + importStarred = self.scope.importStarred + try: + self.scope[node.id].used = (self.scope, node.lineno) + except KeyError: + pass + else: + return + + # try enclosing function scopes + + for scope in self.scopeStack[-2:0:-1]: + importStarred = importStarred or scope.importStarred + if not isinstance(scope, FunctionScope): + continue + try: + scope[node.id].used = (self.scope, node.lineno) + except KeyError: + pass + else: + return + + # try global scope + + importStarred = importStarred or self.scopeStack[0].importStarred + try: + self.scopeStack[0][node.id].used = (self.scope, node.lineno) + except KeyError: + if ((not hasattr(__builtin__, node.id)) + and node.id not in _MAGIC_GLOBALS + and not importStarred): + if (os.path.basename(self.filename) == '__init__.py' and + node.id == '__path__'): + # the special name __path__ is valid only in packages + pass + else: + self.report(messages.UndefinedName, node.lineno, node.id) + elif isinstance(node.ctx, (_ast.Store, _ast.AugStore)): + # if the name hasn't already been defined in the current scope + if isinstance(self.scope, FunctionScope) and node.id not in self.scope: + # for each function or module scope above us + for scope in self.scopeStack[:-1]: + if not isinstance(scope, (FunctionScope, ModuleScope)): + continue + # if the name was defined in that scope, and the name has + # been accessed already in the current scope, and hasn't + # been declared global + if (node.id in scope + and scope[node.id].used + and scope[node.id].used[0] is self.scope + and node.id not in self.scope.globals): + # then it's probably a mistake + self.report(messages.UndefinedLocal, + scope[node.id].used[1], + node.id, + scope[node.id].source.lineno) + break + + if isinstance(node.parent, + (_ast.For, _ast.comprehension, _ast.Tuple, _ast.List)): + binding = Binding(node.id, node) + elif (node.id == '__all__' and + isinstance(self.scope, ModuleScope)): + binding = ExportBinding(node.id, node.parent.value) + else: + binding = Assignment(node.id, node) + if node.id in self.scope: + binding.used = self.scope[node.id].used + self.addBinding(node.lineno, binding) + elif isinstance(node.ctx, _ast.Del): + if isinstance(self.scope, FunctionScope) and \ + node.id in self.scope.globals: + del self.scope.globals[node.id] + else: + self.addBinding(node.lineno, UnBinding(node.id, node)) + else: + # must be a Param context -- this only happens for names in function + # arguments, but these aren't dispatched through here + raise RuntimeError( + "Got impossible expression context: %r" % (node.ctx,)) + + + def FUNCTIONDEF(self, node): + # the decorators attribute is called decorator_list as of Python 2.6 + if hasattr(node, 'decorators'): + for deco in node.decorators: + self.handleNode(deco, node) + else: + for deco in node.decorator_list: + self.handleNode(deco, node) + self.addBinding(node.lineno, FunctionDefinition(node.name, node)) + self.LAMBDA(node) + + def LAMBDA(self, node): + for default in node.args.defaults: + self.handleNode(default, node) + + def runFunction(): + args = [] + + def addArgs(arglist): + for arg in arglist: + if isinstance(arg, _ast.Tuple): + addArgs(arg.elts) + else: + if arg.id in args: + self.report(messages.DuplicateArgument, + node.lineno, arg.id) + args.append(arg.id) + + self.pushFunctionScope() + addArgs(node.args.args) + # vararg/kwarg identifiers are not Name nodes + if node.args.vararg: + args.append(node.args.vararg) + if node.args.kwarg: + args.append(node.args.kwarg) + for name in args: + self.addBinding(node.lineno, Argument(name, node), reportRedef=False) + if isinstance(node.body, list): + # case for FunctionDefs + for stmt in node.body: + self.handleNode(stmt, node) + else: + # case for Lambdas + self.handleNode(node.body, node) + def checkUnusedAssignments(): + """ + Check to see if any assignments have not been used. + """ + for name, binding in self.scope.iteritems(): + if (not binding.used and not name in self.scope.globals + and isinstance(binding, Assignment)): + self.report(messages.UnusedVariable, + binding.source.lineno, name) + self.deferAssignment(checkUnusedAssignments) + self.popScope() + + self.deferFunction(runFunction) + + + def CLASSDEF(self, node): + """ + Check names used in a class definition, including its decorators, base + classes, and the body of its definition. Additionally, add its name to + the current scope. + """ + # decorator_list is present as of Python 2.6 + for deco in getattr(node, 'decorator_list', []): + self.handleNode(deco, node) + for baseNode in node.bases: + self.handleNode(baseNode, node) + self.pushClassScope() + for stmt in node.body: + self.handleNode(stmt, node) + self.popScope() + self.addBinding(node.lineno, Binding(node.name, node)) + + def ASSIGN(self, node): + self.handleNode(node.value, node) + for target in node.targets: + self.handleNode(target, node) + + def AUGASSIGN(self, node): + # AugAssign is awkward: must set the context explicitly and visit twice, + # once with AugLoad context, once with AugStore context + node.target.ctx = _ast.AugLoad() + self.handleNode(node.target, node) + self.handleNode(node.value, node) + node.target.ctx = _ast.AugStore() + self.handleNode(node.target, node) + + def IMPORT(self, node): + for alias in node.names: + name = alias.asname or alias.name + importation = Importation(name, node) + self.addBinding(node.lineno, importation) + + def IMPORTFROM(self, node): + if node.module == '__future__': + if not self.futuresAllowed: + self.report(messages.LateFutureImport, node.lineno, + [n.name for n in node.names]) + else: + self.futuresAllowed = False + + for alias in node.names: + if alias.name == '*': + self.scope.importStarred = True + self.report(messages.ImportStarUsed, node.lineno, node.module) + continue + name = alias.asname or alias.name + importation = Importation(name, node) + if node.module == '__future__': + importation.used = (self.scope, node.lineno) + self.addBinding(node.lineno, importation) diff --git a/flake8/messages.py b/flake8/messages.py new file mode 100644 index 0000000..147b562 --- /dev/null +++ b/flake8/messages.py @@ -0,0 +1,94 @@ +# (c) 2005 Divmod, Inc. See LICENSE file for details + +class Message(object): + message = '' + message_args = () + def __init__(self, filename, lineno): + self.filename = filename + self.lineno = lineno + def __str__(self): + return '%s:%s: %s' % (self.filename, self.lineno, self.message % self.message_args) + + +class UnusedImport(Message): + message = '%r imported but unused' + def __init__(self, filename, lineno, name): + Message.__init__(self, filename, lineno) + self.message_args = (name,) + + +class RedefinedWhileUnused(Message): + message = 'redefinition of unused %r from line %r' + def __init__(self, filename, lineno, name, orig_lineno): + Message.__init__(self, filename, lineno) + self.message_args = (name, orig_lineno) + + +class ImportShadowedByLoopVar(Message): + message = 'import %r from line %r shadowed by loop variable' + def __init__(self, filename, lineno, name, orig_lineno): + Message.__init__(self, filename, lineno) + self.message_args = (name, orig_lineno) + + +class ImportStarUsed(Message): + message = "'from %s import *' used; unable to detect undefined names" + def __init__(self, filename, lineno, modname): + Message.__init__(self, filename, lineno) + self.message_args = (modname,) + + +class UndefinedName(Message): + message = 'undefined name %r' + def __init__(self, filename, lineno, name): + Message.__init__(self, filename, lineno) + self.message_args = (name,) + + + +class UndefinedExport(Message): + message = 'undefined name %r in __all__' + def __init__(self, filename, lineno, name): + Message.__init__(self, filename, lineno) + self.message_args = (name,) + + + +class UndefinedLocal(Message): + message = "local variable %r (defined in enclosing scope on line %r) referenced before assignment" + def __init__(self, filename, lineno, name, orig_lineno): + Message.__init__(self, filename, lineno) + self.message_args = (name, orig_lineno) + + +class DuplicateArgument(Message): + message = 'duplicate argument %r in function definition' + def __init__(self, filename, lineno, name): + Message.__init__(self, filename, lineno) + self.message_args = (name,) + + +class RedefinedFunction(Message): + message = 'redefinition of function %r from line %r' + def __init__(self, filename, lineno, name, orig_lineno): + Message.__init__(self, filename, lineno) + self.message_args = (name, orig_lineno) + + +class LateFutureImport(Message): + message = 'future import(s) %r after other statements' + def __init__(self, filename, lineno, names): + Message.__init__(self, filename, lineno) + self.message_args = (names,) + + +class UnusedVariable(Message): + """ + Indicates that a variable has been explicity assigned to but not actually + used. + """ + + message = 'local variable %r is assigned to but never used' + def __init__(self, filename, lineno, names): + Message.__init__(self, filename, lineno) + self.message_args = (names,) diff --git a/flake8/pep8.py b/flake8/pep8.py new file mode 100644 index 0000000..f40c524 --- /dev/null +++ b/flake8/pep8.py @@ -0,0 +1,1254 @@ +#!/usr/bin/python +# pep8.py - Check Python source code formatting, according to PEP 8 +# Copyright (C) 2006 Johann C. Rocholl +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Check Python source code formatting, according to PEP 8: +http://www.python.org/dev/peps/pep-0008/ + +For usage and a list of options, try this: +$ python pep8.py -h + +This program and its regression test suite live here: +http://github.com/jcrocholl/pep8 + +Groups of errors and warnings: +E errors +W warnings +100 indentation +200 whitespace +300 blank lines +400 imports +500 line length +600 deprecation +700 statements + +You can add checks to this program by writing plugins. Each plugin is +a simple function that is called for each line of source code, either +physical or logical. + +Physical line: +- Raw line of text from the input file. + +Logical line: +- Multi-line statements converted to a single line. +- Stripped left and right. +- Contents of strings replaced with 'xxx' of same length. +- Comments removed. + +The check function requests physical or logical lines by the name of +the first argument: + +def maximum_line_length(physical_line) +def extraneous_whitespace(logical_line) +def blank_lines(logical_line, blank_lines, indent_level, line_number) + +The last example above demonstrates how check plugins can request +additional information with extra arguments. All attributes of the +Checker object are available. Some examples: + +lines: a list of the raw lines from the input file +tokens: the tokens that contribute to this logical line +line_number: line number in the input file +blank_lines: blank lines before this one +indent_char: first indentation character in this file (' ' or '\t') +indent_level: indentation (with tabs expanded to multiples of 8) +previous_indent_level: indentation on previous line +previous_logical: previous logical line + +The docstring of each check function shall be the relevant part of +text from PEP 8. It is printed if the user enables --show-pep8. +Several docstrings contain examples directly from the PEP 8 document. + +Okay: spam(ham[1], {eggs: 2}) +E201: spam( ham[1], {eggs: 2}) + +These examples are verified automatically when pep8.py is run with the +--doctest option. You can add examples for your own check functions. +The format is simple: "Okay" or error/warning code followed by colon +and space, the rest of the line is example source code. If you put 'r' +before the docstring, you can use \n for newline, \t for tab and \s +for space. + +""" + +__version__ = '0.5.0' + +import os +import sys +import re +import time +import inspect +import tokenize +from optparse import OptionParser +from keyword import iskeyword +from fnmatch import fnmatch + +DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' +DEFAULT_IGNORE = ['E24'] + +INDENT_REGEX = re.compile(r'([ \t]*)') +RAISE_COMMA_REGEX = re.compile(r'raise\s+\w+\s*(,)') +SELFTEST_REGEX = re.compile(r'(Okay|[EW]\d{3}):\s(.*)') +ERRORCODE_REGEX = re.compile(r'[EW]\d{3}') +E301NOT_REGEX = re.compile(r'class |def |u?r?["\']') + +WHITESPACE = ' \t' + +BINARY_OPERATORS = ['**=', '*=', '+=', '-=', '!=', '<>', + '%=', '^=', '&=', '|=', '==', '/=', '//=', '>=', '<=', '>>=', '<<=', + '%', '^', '&', '|', '=', '/', '//', '>', '<', '>>', '<<'] +UNARY_OPERATORS = ['**', '*', '+', '-'] +OPERATORS = BINARY_OPERATORS + UNARY_OPERATORS + +options = None +args = None + + +############################################################################## +# Plugins (check functions) for physical lines +############################################################################## + + +def tabs_or_spaces(physical_line, indent_char): + r""" + Never mix tabs and spaces. + + The most popular way of indenting Python is with spaces only. The + second-most popular way is with tabs only. Code indented with a mixture + of tabs and spaces should be converted to using spaces exclusively. When + invoking the Python command line interpreter with the -t option, it issues + warnings about code that illegally mixes tabs and spaces. When using -tt + these warnings become errors. These options are highly recommended! + + Okay: if a == 0:\n a = 1\n b = 1 + E101: if a == 0:\n a = 1\n\tb = 1 + """ + indent = INDENT_REGEX.match(physical_line).group(1) + for offset, char in enumerate(indent): + if char != indent_char: + return offset, "E101 indentation contains mixed spaces and tabs" + + +def tabs_obsolete(physical_line): + r""" + For new projects, spaces-only are strongly recommended over tabs. Most + editors have features that make this easy to do. + + Okay: if True:\n return + W191: if True:\n\treturn + """ + indent = INDENT_REGEX.match(physical_line).group(1) + if indent.count('\t'): + return indent.index('\t'), "W191 indentation contains tabs" + + +def trailing_whitespace(physical_line): + """ + JCR: Trailing whitespace is superfluous. + + Okay: spam(1) + W291: spam(1)\s + """ + physical_line = physical_line.rstrip('\n') # chr(10), newline + physical_line = physical_line.rstrip('\r') # chr(13), carriage return + physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L + stripped = physical_line.rstrip() + if physical_line != stripped: + return len(stripped), "W291 trailing whitespace" + + +def trailing_blank_lines(physical_line, lines, line_number): + r""" + JCR: Trailing blank lines are superfluous. + + Okay: spam(1) + W391: spam(1)\n + """ + if physical_line.strip() == '' and line_number == len(lines): + return 0, "W391 blank line at end of file" + + +def missing_newline(physical_line): + """ + JCR: The last line should have a newline. + """ + if physical_line.rstrip() == physical_line: + return len(physical_line), "W292 no newline at end of file" + + +def maximum_line_length(physical_line): + """ + Limit all lines to a maximum of 79 characters. + + There are still many devices around that are limited to 80 character + lines; plus, limiting windows to 80 characters makes it possible to have + several windows side-by-side. The default wrapping on such devices looks + ugly. Therefore, please limit all lines to a maximum of 79 characters. + For flowing long blocks of text (docstrings or comments), limiting the + length to 72 characters is recommended. + """ + length = len(physical_line.rstrip()) + if length > 79: + return 79, "E501 line too long (%d characters)" % length + + +############################################################################## +# Plugins (check functions) for logical lines +############################################################################## + + +def blank_lines(logical_line, blank_lines, indent_level, line_number, + previous_logical, blank_lines_before_comment): + r""" + Separate top-level function and class definitions with two blank lines. + + Method definitions inside a class are separated by a single blank line. + + Extra blank lines may be used (sparingly) to separate groups of related + functions. Blank lines may be omitted between a bunch of related + one-liners (e.g. a set of dummy implementations). + + Use blank lines in functions, sparingly, to indicate logical sections. + + Okay: def a():\n pass\n\n\ndef b():\n pass + Okay: def a():\n pass\n\n\n# Foo\n# Bar\n\ndef b():\n pass + + E301: class Foo:\n b = 0\n def bar():\n pass + E302: def a():\n pass\n\ndef b(n):\n pass + E303: def a():\n pass\n\n\n\ndef b(n):\n pass + E303: def a():\n\n\n\n pass + E304: @decorator\n\ndef a():\n pass + """ + if line_number == 1: + return # Don't expect blank lines before the first line + max_blank_lines = max(blank_lines, blank_lines_before_comment) + if previous_logical.startswith('@'): + if max_blank_lines: + return 0, "E304 blank lines found after function decorator" + elif max_blank_lines > 2 or (indent_level and max_blank_lines == 2): + return 0, "E303 too many blank lines (%d)" % max_blank_lines + elif (logical_line.startswith('def ') or + logical_line.startswith('class ') or + logical_line.startswith('@')): + if indent_level: + if not (max_blank_lines or E301NOT_REGEX.match(previous_logical)): + return 0, "E301 expected 1 blank line, found 0" + elif max_blank_lines != 2: + return 0, "E302 expected 2 blank lines, found %d" % max_blank_lines + + +def extraneous_whitespace(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + + - Immediately before a comma, semicolon, or colon. + + Okay: spam(ham[1], {eggs: 2}) + E201: spam( ham[1], {eggs: 2}) + E201: spam(ham[ 1], {eggs: 2}) + E201: spam(ham[1], { eggs: 2}) + E202: spam(ham[1], {eggs: 2} ) + E202: spam(ham[1 ], {eggs: 2}) + E202: spam(ham[1], {eggs: 2 }) + + E203: if x == 4: print x, y; x, y = y , x + E203: if x == 4: print x, y ; x, y = y, x + E203: if x == 4 : print x, y; x, y = y, x + """ + line = logical_line + for char in '([{': + found = line.find(char + ' ') + if found > -1: + return found + 1, "E201 whitespace after '%s'" % char + for char in '}])': + found = line.find(' ' + char) + if found > -1 and line[found - 1] != ',': + return found, "E202 whitespace before '%s'" % char + for char in ',;:': + found = line.find(' ' + char) + if found > -1: + return found, "E203 whitespace before '%s'" % char + + +def missing_whitespace(logical_line): + """ + JCR: Each comma, semicolon or colon should be followed by whitespace. + + Okay: [a, b] + Okay: (3,) + Okay: a[1:4] + Okay: a[:4] + Okay: a[1:] + Okay: a[1:4:2] + E231: ['a','b'] + E231: foo(bar,baz) + """ + line = logical_line + for index in range(len(line) - 1): + char = line[index] + if char in ',;:' and line[index + 1] not in WHITESPACE: + before = line[:index] + if char == ':' and before.count('[') > before.count(']'): + continue # Slice syntax, no space required + if char == ',' and line[index + 1] == ')': + continue # Allow tuple with only one element: (3,) + return index, "E231 missing whitespace after '%s'" % char + + +def indentation(logical_line, previous_logical, indent_char, + indent_level, previous_indent_level): + r""" + Use 4 spaces per indentation level. + + For really old code that you don't want to mess up, you can continue to + use 8-space tabs. + + Okay: a = 1 + Okay: if a == 0:\n a = 1 + E111: a = 1 + + Okay: for item in items:\n pass + E112: for item in items:\npass + + Okay: a = 1\nb = 2 + E113: a = 1\n b = 2 + """ + if indent_char == ' ' and indent_level % 4: + return 0, "E111 indentation is not a multiple of four" + indent_expect = previous_logical.endswith(':') + if indent_expect and indent_level <= previous_indent_level: + return 0, "E112 expected an indented block" + if indent_level > previous_indent_level and not indent_expect: + return 0, "E113 unexpected indentation" + + +def whitespace_before_parameters(logical_line, tokens): + """ + Avoid extraneous whitespace in the following situations: + + - Immediately before the open parenthesis that starts the argument + list of a function call. + + - Immediately before the open parenthesis that starts an indexing or + slicing. + + Okay: spam(1) + E211: spam (1) + + Okay: dict['key'] = list[index] + E211: dict ['key'] = list[index] + E211: dict['key'] = list [index] + """ + prev_type = tokens[0][0] + prev_text = tokens[0][1] + prev_end = tokens[0][3] + for index in range(1, len(tokens)): + token_type, text, start, end, line = tokens[index] + if (token_type == tokenize.OP and + text in '([' and + start != prev_end and + prev_type == tokenize.NAME and + (index < 2 or tokens[index - 2][1] != 'class') and + (not iskeyword(prev_text))): + return prev_end, "E211 whitespace before '%s'" % text + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_operator(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + Okay: a = 12 + 3 + E221: a = 4 + 5 + E222: a = 4 + 5 + E223: a = 4\t+ 5 + E224: a = 4 +\t5 + """ + line = logical_line + for operator in OPERATORS: + found = line.find(' ' + operator) + if found > -1: + return found, "E221 multiple spaces before operator" + found = line.find(operator + ' ') + if found > -1: + return found, "E222 multiple spaces after operator" + found = line.find('\t' + operator) + if found > -1: + return found, "E223 tab before operator" + found = line.find(operator + '\t') + if found > -1: + return found, "E224 tab after operator" + + +def missing_whitespace_around_operator(logical_line, tokens): + r""" + - Always surround these binary operators with a single space on + either side: assignment (=), augmented assignment (+=, -= etc.), + comparisons (==, <, >, !=, <>, <=, >=, in, not in, is, is not), + Booleans (and, or, not). + + - Use spaces around arithmetic operators. + + Okay: i = i + 1 + Okay: submitted += 1 + Okay: x = x * 2 - 1 + Okay: hypot2 = x * x + y * y + Okay: c = (a + b) * (a - b) + Okay: foo(bar, key='word', *args, **kwargs) + Okay: baz(**kwargs) + Okay: negative = -1 + Okay: spam(-1) + Okay: alpha[:-i] + Okay: if not -5 < x < +5:\n pass + Okay: lambda *args, **kw: (args, kw) + + E225: i=i+1 + E225: submitted +=1 + E225: x = x*2 - 1 + E225: hypot2 = x*x + y*y + E225: c = (a+b) * (a-b) + E225: c = alpha -4 + E225: z = x **y + """ + parens = 0 + need_space = False + prev_type = tokenize.OP + prev_text = prev_end = None + for token_type, text, start, end, line in tokens: + if token_type in (tokenize.NL, tokenize.NEWLINE, tokenize.ERRORTOKEN): + # ERRORTOKEN is triggered by backticks in Python 3000 + continue + if text in ('(', 'lambda'): + parens += 1 + elif text == ')': + parens -= 1 + if need_space: + if start == prev_end: + return prev_end, "E225 missing whitespace around operator" + need_space = False + elif token_type == tokenize.OP: + if text == '=' and parens: + # Allow keyword args or defaults: foo(bar=None). + pass + elif text in BINARY_OPERATORS: + need_space = True + elif text in UNARY_OPERATORS: + if ((prev_type != tokenize.OP or prev_text in '}])') and not + (prev_type == tokenize.NAME and iskeyword(prev_text))): + # Allow unary operators: -123, -x, +1. + # Allow argument unpacking: foo(*args, **kwargs). + need_space = True + if need_space and start == prev_end: + return prev_end, "E225 missing whitespace around operator" + prev_type = token_type + prev_text = text + prev_end = end + + +def whitespace_around_comma(logical_line): + """ + Avoid extraneous whitespace in the following situations: + + - More than one space around an assignment (or other) operator to + align it with another. + + JCR: This should also be applied around comma etc. + Note: these checks are disabled by default + + Okay: a = (1, 2) + E241: a = (1, 2) + E242: a = (1,\t2) + """ + line = logical_line + for separator in ',;:': + found = line.find(separator + ' ') + if found > -1: + return found + 1, "E241 multiple spaces after '%s'" % separator + found = line.find(separator + '\t') + if found > -1: + return found + 1, "E242 tab after '%s'" % separator + + +def whitespace_around_named_parameter_equals(logical_line): + """ + Don't use spaces around the '=' sign when used to indicate a + keyword argument or a default parameter value. + + Okay: def complex(real, imag=0.0): + Okay: return magic(r=real, i=imag) + Okay: boolean(a == b) + Okay: boolean(a != b) + Okay: boolean(a <= b) + Okay: boolean(a >= b) + + E251: def complex(real, imag = 0.0): + E251: return magic(r = real, i = imag) + """ + parens = 0 + window = ' ' + equal_ok = ['==', '!=', '<=', '>='] + + for pos, c in enumerate(logical_line): + window = window[1:] + c + if parens: + if window[0] in WHITESPACE and window[1] == '=': + if window[1:] not in equal_ok: + issue = "E251 no spaces around keyword / parameter equals" + return pos, issue + if window[2] in WHITESPACE and window[1] == '=': + if window[:2] not in equal_ok: + issue = "E251 no spaces around keyword / parameter equals" + return pos, issue + if c == '(': + parens += 1 + elif c == ')': + parens -= 1 + + +def whitespace_before_inline_comment(logical_line, tokens): + """ + Separate inline comments by at least two spaces. + + An inline comment is a comment on the same line as a statement. Inline + comments should be separated by at least two spaces from the statement. + They should start with a # and a single space. + + Okay: x = x + 1 # Increment x + Okay: x = x + 1 # Increment x + E261: x = x + 1 # Increment x + E262: x = x + 1 #Increment x + E262: x = x + 1 # Increment x + """ + prev_end = (0, 0) + for token_type, text, start, end, line in tokens: + if token_type == tokenize.NL: + continue + if token_type == tokenize.COMMENT: + if not line[:start[1]].strip(): + continue + if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: + return (prev_end, + "E261 at least two spaces before inline comment") + if (len(text) > 1 and text.startswith('# ') + or not text.startswith('# ')): + return start, "E262 inline comment should start with '# '" + else: + prev_end = end + + +def imports_on_separate_lines(logical_line): + r""" + Imports should usually be on separate lines. + + Okay: import os\nimport sys + E401: import sys, os + + Okay: from subprocess import Popen, PIPE + Okay: from myclas import MyClass + Okay: from foo.bar.yourclass import YourClass + Okay: import myclass + Okay: import foo.bar.yourclass + """ + line = logical_line + if line.startswith('import '): + found = line.find(',') + if found > -1: + return found, "E401 multiple imports on one line" + + +def compound_statements(logical_line): + r""" + Compound statements (multiple statements on the same line) are + generally discouraged. + + While sometimes it's okay to put an if/for/while with a small body + on the same line, never do this for multi-clause statements. Also + avoid folding such long lines! + + Okay: if foo == 'blah':\n do_blah_thing() + Okay: do_one() + Okay: do_two() + Okay: do_three() + + E701: if foo == 'blah': do_blah_thing() + E701: for x in lst: total += x + E701: while t < 10: t = delay() + E701: if foo == 'blah': do_blah_thing() + E701: else: do_non_blah_thing() + E701: try: something() + E701: finally: cleanup() + E701: if foo == 'blah': one(); two(); three() + + E702: do_one(); do_two(); do_three() + """ + line = logical_line + found = line.find(':') + if -1 < found < len(line) - 1: + before = line[:found] + if (before.count('{') <= before.count('}') and # {'a': 1} (dict) + before.count('[') <= before.count(']') and # [1:2] (slice) + not re.search(r'\blambda\b', before)): # lambda x: x + return found, "E701 multiple statements on one line (colon)" + found = line.find(';') + if -1 < found: + return found, "E702 multiple statements on one line (semicolon)" + + +def python_3000_has_key(logical_line): + """ + The {}.has_key() method will be removed in the future version of + Python. Use the 'in' operation instead, like: + d = {"a": 1, "b": 2} + if "b" in d: + print d["b"] + """ + pos = logical_line.find('.has_key(') + if pos > -1: + return pos, "W601 .has_key() is deprecated, use 'in'" + + +def python_3000_raise_comma(logical_line): + """ + When raising an exception, use "raise ValueError('message')" + instead of the older form "raise ValueError, 'message'". + + The paren-using form is preferred because when the exception arguments + are long or include string formatting, you don't need to use line + continuation characters thanks to the containing parentheses. The older + form will be removed in Python 3000. + """ + match = RAISE_COMMA_REGEX.match(logical_line) + if match: + return match.start(1), "W602 deprecated form of raising exception" + + +def python_3000_not_equal(logical_line): + """ + != can also be written <>, but this is an obsolete usage kept for + backwards compatibility only. New code should always use !=. + The older syntax is removed in Python 3000. + """ + pos = logical_line.find('<>') + if pos > -1: + return pos, "W603 '<>' is deprecated, use '!='" + + +def python_3000_backticks(logical_line): + """ + Backticks are removed in Python 3000. + Use repr() instead. + """ + pos = logical_line.find('`') + if pos > -1: + return pos, "W604 backticks are deprecated, use 'repr()'" + + +############################################################################## +# Helper functions +############################################################################## + + +def expand_indent(line): + """ + Return the amount of indentation. + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\\t') + 8 + >>> expand_indent(' \\t') + 8 + >>> expand_indent(' \\t') + 8 + >>> expand_indent(' \\t') + 16 + """ + result = 0 + for char in line: + if char == '\t': + result = result // 8 * 8 + 8 + elif char == ' ': + result += 1 + else: + break + return result + + +def mute_string(text): + """ + Replace contents with 'xxx' to prevent syntax matching. + + >>> mute_string('"abc"') + '"xxx"' + >>> mute_string("'''abc'''") + "'''xxx'''" + >>> mute_string("r'abc'") + "r'xxx'" + """ + start = 1 + end = len(text) - 1 + # String modifiers (e.g. u or r) + if text.endswith('"'): + start += text.index('"') + elif text.endswith("'"): + start += text.index("'") + # Triple quotes + if text.endswith('"""') or text.endswith("'''"): + start += 2 + end -= 2 + return text[:start] + 'x' * (end - start) + text[end:] + + +def message(text): + """Print a message.""" + # print >> sys.stderr, options.prog + ': ' + text + # print >> sys.stderr, text + print(text) + + +############################################################################## +# Framework to run all checks +############################################################################## + + +def find_checks(argument_name): + """ + Find all globally visible functions where the first argument name + starts with argument_name. + """ + checks = [] + for name, function in globals().items(): + if not inspect.isfunction(function): + continue + args = inspect.getargspec(function)[0] + if args and args[0].startswith(argument_name): + codes = ERRORCODE_REGEX.findall(inspect.getdoc(function) or '') + for code in codes or ['']: + if not code or not ignore_code(code): + checks.append((name, function, args)) + break + checks.sort() + return checks + + +class Checker(object): + """ + Load a Python source file, tokenize it, check coding style. + """ + + def __init__(self, filename): + if filename: + self.filename = filename + try: + self.lines = open(filename).readlines() + except UnicodeDecodeError: + # Errors may occur with non-UTF8 files in Python 3000 + self.lines = open(filename, errors='replace').readlines() + else: + self.filename = 'stdin' + self.lines = [] + options.counters['physical lines'] = \ + options.counters.get('physical lines', 0) + len(self.lines) + + def readline(self): + """ + Get the next line from the input buffer. + """ + self.line_number += 1 + if self.line_number > len(self.lines): + return '' + return self.lines[self.line_number - 1] + + def readline_check_physical(self): + """ + Check and return the next physical line. This method can be + used to feed tokenize.generate_tokens. + """ + line = self.readline() + if line: + self.check_physical(line) + return line + + def run_check(self, check, argument_names): + """ + Run a check plugin. + """ + arguments = [] + for name in argument_names: + arguments.append(getattr(self, name)) + return check(*arguments) + + def check_physical(self, line): + """ + Run all physical checks on a raw input line. + """ + self.physical_line = line + if self.indent_char is None and len(line) and line[0] in ' \t': + self.indent_char = line[0] + for name, check, argument_names in options.physical_checks: + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + self.report_error(self.line_number, offset, text, check) + + def build_tokens_line(self): + """ + Build a logical line from tokens. + """ + self.mapping = [] + logical = [] + length = 0 + previous = None + for token in self.tokens: + token_type, text = token[0:2] + if token_type in (tokenize.COMMENT, tokenize.NL, + tokenize.INDENT, tokenize.DEDENT, + tokenize.NEWLINE): + continue + if token_type == tokenize.STRING: + text = mute_string(text) + if previous: + end_line, end = previous[3] + start_line, start = token[2] + if end_line != start_line: # different row + if self.lines[end_line - 1][end - 1] not in '{[(': + logical.append(' ') + length += 1 + elif end != start: # different column + fill = self.lines[end_line - 1][end:start] + logical.append(fill) + length += len(fill) + self.mapping.append((length, token)) + logical.append(text) + length += len(text) + previous = token + self.logical_line = ''.join(logical) + assert self.logical_line.lstrip() == self.logical_line + assert self.logical_line.rstrip() == self.logical_line + + def check_logical(self): + """ + Build a line from tokens and run all logical checks on it. + """ + options.counters['logical lines'] = \ + options.counters.get('logical lines', 0) + 1 + self.build_tokens_line() + first_line = self.lines[self.mapping[0][1][2][0] - 1] + indent = first_line[:self.mapping[0][1][2][1]] + self.previous_indent_level = self.indent_level + self.indent_level = expand_indent(indent) + if options.verbose >= 2: + print(self.logical_line[:80].rstrip()) + for name, check, argument_names in options.logical_checks: + if options.verbose >= 3: + print(' ', name) + result = self.run_check(check, argument_names) + if result is not None: + offset, text = result + if isinstance(offset, tuple): + original_number, original_offset = offset + else: + for token_offset, token in self.mapping: + if offset >= token_offset: + original_number = token[2][0] + original_offset = (token[2][1] + + offset - token_offset) + self.report_error(original_number, original_offset, + text, check) + self.previous_logical = self.logical_line + + def check_all(self): + """ + Run all checks on the input file. + """ + self.file_errors = 0 + self.line_number = 0 + self.indent_char = None + self.indent_level = 0 + self.previous_logical = '' + self.blank_lines = 0 + self.blank_lines_before_comment = 0 + self.tokens = [] + parens = 0 + for token in tokenize.generate_tokens(self.readline_check_physical): + # print(tokenize.tok_name[token[0]], repr(token)) + self.tokens.append(token) + token_type, text = token[0:2] + if token_type == tokenize.OP and text in '([{': + parens += 1 + if token_type == tokenize.OP and text in '}])': + parens -= 1 + if token_type == tokenize.NEWLINE and not parens: + self.check_logical() + self.blank_lines = 0 + self.blank_lines_before_comment = 0 + self.tokens = [] + if token_type == tokenize.NL and not parens: + if len(self.tokens) <= 1: + # The physical line contains only this token. + self.blank_lines += 1 + self.tokens = [] + if token_type == tokenize.COMMENT: + source_line = token[4] + token_start = token[2][1] + if source_line[:token_start].strip() == '': + self.blank_lines_before_comment = max(self.blank_lines, + self.blank_lines_before_comment) + self.blank_lines = 0 + if text.endswith('\n') and not parens: + # The comment also ends a physical line. This works around + # Python < 2.6 behaviour, which does not generate NL after + # a comment which is on a line by itself. + self.tokens = [] + return self.file_errors + + def report_error(self, line_number, offset, text, check): + """ + Report an error, according to options. + """ + if options.quiet == 1 and not self.file_errors: + message(self.filename) + self.file_errors += 1 + code = text[:4] + options.counters[code] = options.counters.get(code, 0) + 1 + options.messages[code] = text[5:] + if options.quiet: + return + if options.testsuite: + basename = os.path.basename(self.filename) + if basename[:4] != code: + return # Don't care about other errors or warnings + if 'not' not in basename: + return # Don't print the expected error message + if ignore_code(code): + return + if options.counters[code] == 1 or options.repeat: + message("%s:%s:%d: %s" % + (self.filename, line_number, offset + 1, text)) + if options.show_source: + line = self.lines[line_number - 1] + message(line.rstrip()) + message(' ' * offset + '^') + if options.show_pep8: + message(check.__doc__.lstrip('\n').rstrip()) + + +def input_file(filename): + """ + Run all checks on a Python source file. + """ + if excluded(filename): + return {} + if options.verbose: + message('checking ' + filename) + files_counter_before = options.counters.get('files', 0) + if options.testsuite: # Keep showing errors for multiple tests + options.counters = {} + options.counters['files'] = files_counter_before + 1 + errors = Checker(filename).check_all() + if options.testsuite: # Check if the expected error was found + basename = os.path.basename(filename) + code = basename[:4] + count = options.counters.get(code, 0) + if count == 0 and 'not' not in basename: + message("%s: error %s not found" % (filename, code)) + return len(errors) + +def input_dir(dirname): + """ + Check all Python source files in this directory and all subdirectories. + """ + dirname = dirname.rstrip('/') + if excluded(dirname): + return + for root, dirs, files in os.walk(dirname): + if options.verbose: + message('directory ' + root) + options.counters['directories'] = \ + options.counters.get('directories', 0) + 1 + dirs.sort() + for subdir in dirs: + if excluded(subdir): + dirs.remove(subdir) + files.sort() + for filename in files: + if filename_match(filename): + input_file(os.path.join(root, filename)) + + +def excluded(filename): + """ + Check if options.exclude contains a pattern that matches filename. + """ + basename = os.path.basename(filename) + for pattern in options.exclude: + if fnmatch(basename, pattern): + # print basename, 'excluded because it matches', pattern + return True + + +def filename_match(filename): + """ + Check if options.filename contains a pattern that matches filename. + If options.filename is unspecified, this always returns True. + """ + if not options.filename: + return True + for pattern in options.filename: + if fnmatch(filename, pattern): + return True + + +def ignore_code(code): + """ + Check if options.ignore contains a prefix of the error code. + If options.select contains a prefix of the error code, do not ignore it. + """ + for select in options.select: + if code.startswith(select): + return False + for ignore in options.ignore: + if code.startswith(ignore): + return True + + +def get_error_statistics(): + """Get error statistics.""" + return get_statistics("E") + + +def get_warning_statistics(): + """Get warning statistics.""" + return get_statistics("W") + + +def get_statistics(prefix=''): + """ + Get statistics for message codes that start with the prefix. + + prefix='' matches all errors and warnings + prefix='E' matches all errors + prefix='W' matches all warnings + prefix='E4' matches all errors that have to do with imports + """ + stats = [] + keys = list(options.messages.keys()) + keys.sort() + for key in keys: + if key.startswith(prefix): + stats.append('%-7s %s %s' % + (options.counters[key], key, options.messages[key])) + return stats + + +def get_count(prefix=''): + """Return the total count of errors and warnings.""" + keys = list(options.messages.keys()) + count = 0 + for key in keys: + if key.startswith(prefix): + count += options.counters[key] + return count + + +def print_statistics(prefix=''): + """Print overall statistics (number of errors and warnings).""" + for line in get_statistics(prefix): + print(line) + + +def print_benchmark(elapsed): + """ + Print benchmark numbers. + """ + print('%-7.2f %s' % (elapsed, 'seconds elapsed')) + keys = ['directories', 'files', + 'logical lines', 'physical lines'] + for key in keys: + if key in options.counters: + print('%-7d %s per second (%d total)' % ( + options.counters[key] / elapsed, key, + options.counters[key])) + + +def selftest(): + """ + Test all check functions with test cases in docstrings. + """ + count_passed = 0 + count_failed = 0 + checks = options.physical_checks + options.logical_checks + for name, check, argument_names in checks: + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match is None: + continue + code, source = match.groups() + checker = Checker(None) + for part in source.split(r'\n'): + part = part.replace(r'\t', '\t') + part = part.replace(r'\s', ' ') + checker.lines.append(part + '\n') + options.quiet = 2 + options.counters = {} + checker.check_all() + error = None + if code == 'Okay': + if len(options.counters) > 1: + codes = [key for key in options.counters.keys() + if key != 'logical lines'] + error = "incorrectly found %s" % ', '.join(codes) + elif options.counters.get(code, 0) == 0: + error = "failed to find %s" % code + if not error: + count_passed += 1 + else: + count_failed += 1 + if len(checker.lines) == 1: + print("pep8.py: %s: %s" % + (error, checker.lines[0].rstrip())) + else: + print("pep8.py: %s:" % error) + for line in checker.lines: + print(line.rstrip()) + if options.verbose: + print("%d passed and %d failed." % (count_passed, count_failed)) + if count_failed: + print("Test failed.") + else: + print("Test passed.") + + +def process_options(arglist=None): + """ + Process options passed either via arglist or via command line args. + """ + global options, args + parser = OptionParser(version=__version__, + usage="%prog [options] input ...") + parser.add_option('-v', '--verbose', default=0, action='count', + help="print status messages, or debug with -vv") + parser.add_option('-q', '--quiet', default=0, action='count', + help="report only file names, or nothing with -qq") + parser.add_option('-r', '--repeat', action='store_true', + help="show all occurrences of the same error") + parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, + help="exclude files or directories which match these " + "comma separated patterns (default: %s)" % + DEFAULT_EXCLUDE) + parser.add_option('--filename', metavar='patterns', default='*.py', + help="when parsing directories, only check filenames " + "matching these comma separated patterns (default: " + "*.py)") + parser.add_option('--select', metavar='errors', default='', + help="select errors and warnings (e.g. E,W6)") + parser.add_option('--ignore', metavar='errors', default='', + help="skip errors and warnings (e.g. E4,W)") + parser.add_option('--show-source', action='store_true', + help="show source code for each error") + parser.add_option('--show-pep8', action='store_true', + help="show text of PEP 8 for each error") + parser.add_option('--statistics', action='store_true', + help="count errors and warnings") + parser.add_option('--count', action='store_true', + help="print total number of errors and warnings " + "to standard error and set exit code to 1 if " + "total is not null") + parser.add_option('--benchmark', action='store_true', + help="measure processing speed") + parser.add_option('--testsuite', metavar='dir', + help="run regression tests from dir") + parser.add_option('--doctest', action='store_true', + help="run doctest on myself") + options, args = parser.parse_args(arglist) + if options.testsuite: + args.append(options.testsuite) + if len(args) == 0 and not options.doctest: + parser.error('input not specified') + options.prog = os.path.basename(sys.argv[0]) + options.exclude = options.exclude.split(',') + for index in range(len(options.exclude)): + options.exclude[index] = options.exclude[index].rstrip('/') + if options.filename: + options.filename = options.filename.split(',') + if options.select: + options.select = options.select.split(',') + else: + options.select = [] + if options.ignore: + options.ignore = options.ignore.split(',') + elif options.select: + # Ignore all checks which are not explicitly selected + options.ignore = [''] + elif options.testsuite or options.doctest: + # For doctest and testsuite, all checks are required + options.ignore = [] + else: + # The default choice: ignore controversial checks + options.ignore = DEFAULT_IGNORE + options.physical_checks = find_checks('physical_line') + options.logical_checks = find_checks('logical_line') + options.counters = {} + options.messages = {} + return options, args + + +def _main(): + """ + Parse options and run checks on Python source. + """ + options, args = process_options() + if options.doctest: + import doctest + doctest.testmod(verbose=options.verbose) + selftest() + start_time = time.time() + for path in args: + if os.path.isdir(path): + input_dir(path) + else: + input_file(path) + elapsed = time.time() - start_time + if options.statistics: + print_statistics() + if options.benchmark: + print_benchmark(elapsed) + if options.count: + count = get_count() + if count: + sys.stderr.write(str(count) + '\n') + sys.exit(1) + + +if __name__ == '__main__': + _main() diff --git a/flake8/scripts/__init__.py b/flake8/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flake8/scripts/flake8.py b/flake8/scripts/flake8.py new file mode 100644 index 0000000..2566502 --- /dev/null +++ b/flake8/scripts/flake8.py @@ -0,0 +1,99 @@ + +""" +Implementation of the command-line I{flake8} tool. +""" + +import sys +import os +import _ast + +from flake8.pep8 import input_file +from flake8 import pep8 + +checker = __import__('flake8.checker').checker + +def check(codeString, filename): + """ + Check the Python source given by C{codeString} for flakes. + + @param codeString: The Python source to check. + @type codeString: C{str} + + @param filename: The name of the file the source came from, used to report + errors. + @type filename: C{str} + + @return: The number of warnings emitted. + @rtype: C{int} + """ + # First, compile into an AST and handle syntax errors. + try: + tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) + except SyntaxError, value: + msg = value.args[0] + + (lineno, offset, text) = value.lineno, value.offset, value.text + + # If there's an encoding problem with the file, the text is None. + if text is None: + # Avoid using msg, since for the only known case, it contains a + # bogus message that claims the encoding the file declared was + # unknown. + print >> sys.stderr, "%s: problem decoding source" % (filename, ) + else: + line = text.splitlines()[-1] + + if offset is not None: + offset = offset - (len(text) - len(line)) + + print >> sys.stderr, '%s:%d: %s' % (filename, lineno, msg) + print >> sys.stderr, line + + if offset is not None: + print >> sys.stderr, " " * offset, "^" + + return 1 + else: + # Okay, it's syntactically valid. Now check it. + w = checker.Checker(tree, filename) + w.messages.sort(lambda a, b: cmp(a.lineno, b.lineno)) + for warning in w.messages: + print warning + return len(w.messages) + + +def checkPath(filename): + """ + Check the given path, printing out any warnings detected. + + @return: the number of warnings printed + """ + try: + return check(file(filename, 'U').read() + '\n', filename) + except IOError, msg: + print >> sys.stderr, "%s: %s" % (filename, msg.args[1]) + return 1 + + +def main(): + 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 filename.endswith('.py'): + fullpath = os.path.join(dirpath, filename) + warnings += checkPath(fullpath)) + warnings += input_file(fullpath) + else: + warnings += checkPath(arg) + warnings += input_file(arg) + + else: + stdin = sys.stdin.read() + warnings += check(stdin, '') + + + raise SystemExit(warnings > 0) diff --git a/flake8/test/__init__.py b/flake8/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flake8/test/harness.py b/flake8/test/harness.py new file mode 100644 index 0000000..7cd2277 --- /dev/null +++ b/flake8/test/harness.py @@ -0,0 +1,27 @@ + +import textwrap +import _ast + +from twisted.trial import unittest + +from pyflakes import checker + + +class Test(unittest.TestCase): + + def flakes(self, input, *expectedOutputs, **kw): + ast = compile(textwrap.dedent(input), "", "exec", + _ast.PyCF_ONLY_AST) + w = checker.Checker(ast, **kw) + outputs = [type(o) for o in w.messages] + expectedOutputs = list(expectedOutputs) + outputs.sort() + expectedOutputs.sort() + self.assert_(outputs == expectedOutputs, '''\ +for input: +%s +expected outputs: +%s +but got: +%s''' % (input, repr(expectedOutputs), '\n'.join([str(o) for o in w.messages]))) + return w diff --git a/flake8/test/test_imports.py b/flake8/test/test_imports.py new file mode 100644 index 0000000..08e4580 --- /dev/null +++ b/flake8/test/test_imports.py @@ -0,0 +1,673 @@ + +from sys import version_info + +from pyflakes import messages as m +from pyflakes.test import harness + +class Test(harness.Test): + + def test_unusedImport(self): + self.flakes('import fu, bar', m.UnusedImport, m.UnusedImport) + self.flakes('from baz import fu, bar', m.UnusedImport, m.UnusedImport) + + def test_aliasedImport(self): + self.flakes('import fu as FU, bar as FU', m.RedefinedWhileUnused, m.UnusedImport) + self.flakes('from moo import fu as FU, bar as FU', m.RedefinedWhileUnused, m.UnusedImport) + + def test_usedImport(self): + self.flakes('import fu; print fu') + self.flakes('from baz import fu; print fu') + + def test_redefinedWhileUnused(self): + self.flakes('import fu; fu = 3', m.RedefinedWhileUnused) + self.flakes('import fu; del fu', m.RedefinedWhileUnused) + self.flakes('import fu; fu, bar = 3', m.RedefinedWhileUnused) + self.flakes('import fu; [fu, bar] = 3', m.RedefinedWhileUnused) + + def test_redefinedByFunction(self): + self.flakes(''' + import fu + def fu(): + pass + ''', m.RedefinedWhileUnused) + + def test_redefinedInNestedFunction(self): + """ + Test that shadowing a global name with a nested function definition + generates a warning. + """ + self.flakes(''' + import fu + def bar(): + def baz(): + def fu(): + pass + ''', m.RedefinedWhileUnused, m.UnusedImport) + + def test_redefinedByClass(self): + self.flakes(''' + import fu + class fu: + pass + ''', m.RedefinedWhileUnused) + + + def test_redefinedBySubclass(self): + """ + If an imported name is redefined by a class statement which also uses + that name in the bases list, no warning is emitted. + """ + self.flakes(''' + from fu import bar + class bar(bar): + pass + ''') + + + def test_redefinedInClass(self): + """ + Test that shadowing a global with a class attribute does not produce a + warning. + """ + self.flakes(''' + import fu + class bar: + fu = 1 + print fu + ''') + + def test_usedInFunction(self): + self.flakes(''' + import fu + def fun(): + print fu + ''') + + def test_shadowedByParameter(self): + self.flakes(''' + import fu + def fun(fu): + print fu + ''', m.UnusedImport) + + self.flakes(''' + import fu + def fun(fu): + print fu + print fu + ''') + + def test_newAssignment(self): + self.flakes('fu = None') + + def test_usedInGetattr(self): + self.flakes('import fu; fu.bar.baz') + self.flakes('import fu; "bar".fu.baz', m.UnusedImport) + + def test_usedInSlice(self): + self.flakes('import fu; print fu.bar[1:]') + + def test_usedInIfBody(self): + self.flakes(''' + import fu + if True: print fu + ''') + + def test_usedInIfConditional(self): + self.flakes(''' + import fu + if fu: pass + ''') + + def test_usedInElifConditional(self): + self.flakes(''' + import fu + if False: pass + elif fu: pass + ''') + + def test_usedInElse(self): + self.flakes(''' + import fu + if False: pass + else: print fu + ''') + + def test_usedInCall(self): + self.flakes('import fu; fu.bar()') + + def test_usedInClass(self): + self.flakes(''' + import fu + class bar: + bar = fu + ''') + + def test_usedInClassBase(self): + self.flakes(''' + import fu + class bar(object, fu.baz): + pass + ''') + + def test_notUsedInNestedScope(self): + self.flakes(''' + import fu + def bleh(): + pass + print fu + ''') + + def test_usedInFor(self): + self.flakes(''' + import fu + for bar in range(9): + print fu + ''') + + def test_usedInForElse(self): + self.flakes(''' + import fu + for bar in range(10): + pass + else: + print fu + ''') + + def test_redefinedByFor(self): + self.flakes(''' + import fu + for fu in range(2): + pass + ''', m.RedefinedWhileUnused) + + def test_shadowedByFor(self): + """ + Test that shadowing a global name with a for loop variable generates a + warning. + """ + self.flakes(''' + import fu + fu.bar() + for fu in (): + pass + ''', m.ImportShadowedByLoopVar) + + def test_shadowedByForDeep(self): + """ + Test that shadowing a global name with a for loop variable nested in a + tuple unpack generates a warning. + """ + self.flakes(''' + import fu + fu.bar() + for (x, y, z, (a, b, c, (fu,))) in (): + pass + ''', m.ImportShadowedByLoopVar) + + def test_usedInReturn(self): + self.flakes(''' + import fu + def fun(): + return fu + ''') + + def test_usedInOperators(self): + self.flakes('import fu; 3 + fu.bar') + self.flakes('import fu; 3 % fu.bar') + self.flakes('import fu; 3 - fu.bar') + self.flakes('import fu; 3 * fu.bar') + self.flakes('import fu; 3 ** fu.bar') + self.flakes('import fu; 3 / fu.bar') + self.flakes('import fu; 3 // fu.bar') + self.flakes('import fu; -fu.bar') + self.flakes('import fu; ~fu.bar') + self.flakes('import fu; 1 == fu.bar') + self.flakes('import fu; 1 | fu.bar') + self.flakes('import fu; 1 & fu.bar') + self.flakes('import fu; 1 ^ fu.bar') + self.flakes('import fu; 1 >> fu.bar') + self.flakes('import fu; 1 << fu.bar') + + def test_usedInAssert(self): + self.flakes('import fu; assert fu.bar') + + def test_usedInSubscript(self): + self.flakes('import fu; fu.bar[1]') + + def test_usedInLogic(self): + self.flakes('import fu; fu and False') + self.flakes('import fu; fu or False') + self.flakes('import fu; not fu.bar') + + def test_usedInList(self): + self.flakes('import fu; [fu]') + + def test_usedInTuple(self): + self.flakes('import fu; (fu,)') + + def test_usedInTry(self): + self.flakes(''' + import fu + try: fu + except: pass + ''') + + def test_usedInExcept(self): + self.flakes(''' + import fu + try: fu + except: pass + ''') + + def test_redefinedByExcept(self): + self.flakes(''' + import fu + try: pass + except Exception, fu: pass + ''', m.RedefinedWhileUnused) + + def test_usedInRaise(self): + self.flakes(''' + import fu + raise fu.bar + ''') + + def test_usedInYield(self): + self.flakes(''' + import fu + def gen(): + yield fu + ''') + + def test_usedInDict(self): + self.flakes('import fu; {fu:None}') + self.flakes('import fu; {1:fu}') + + def test_usedInParameterDefault(self): + self.flakes(''' + import fu + def f(bar=fu): + pass + ''') + + def test_usedInAttributeAssign(self): + self.flakes('import fu; fu.bar = 1') + + def test_usedInKeywordArg(self): + self.flakes('import fu; fu.bar(stuff=fu)') + + def test_usedInAssignment(self): + self.flakes('import fu; bar=fu') + self.flakes('import fu; n=0; n+=fu') + + def test_usedInListComp(self): + self.flakes('import fu; [fu for _ in range(1)]') + self.flakes('import fu; [1 for _ in range(1) if fu]') + + def test_redefinedByListComp(self): + self.flakes('import fu; [1 for fu in range(1)]', m.RedefinedWhileUnused) + + + def test_usedInTryFinally(self): + self.flakes(''' + import fu + try: pass + finally: fu + ''') + + self.flakes(''' + import fu + try: fu + finally: pass + ''') + + def test_usedInWhile(self): + self.flakes(''' + import fu + while 0: + fu + ''') + + self.flakes(''' + import fu + while fu: pass + ''') + + def test_usedInGlobal(self): + self.flakes(''' + import fu + def f(): global fu + ''', m.UnusedImport) + + def test_usedInBackquote(self): + self.flakes('import fu; `fu`') + + def test_usedInExec(self): + self.flakes('import fu; exec "print 1" in fu.bar') + + def test_usedInLambda(self): + self.flakes('import fu; lambda: fu') + + def test_shadowedByLambda(self): + self.flakes('import fu; lambda fu: fu', m.UnusedImport) + + def test_usedInSliceObj(self): + self.flakes('import fu; "meow"[::fu]') + + def test_unusedInNestedScope(self): + self.flakes(''' + def bar(): + import fu + fu + ''', m.UnusedImport, m.UndefinedName) + + def test_methodsDontUseClassScope(self): + self.flakes(''' + class bar: + import fu + def fun(self): + fu + ''', m.UnusedImport, m.UndefinedName) + + def test_nestedFunctionsNestScope(self): + self.flakes(''' + def a(): + def b(): + fu + import fu + ''') + + def test_nestedClassAndFunctionScope(self): + self.flakes(''' + def a(): + import fu + class b: + def c(self): + print fu + ''') + + def test_importStar(self): + self.flakes('from fu import *', m.ImportStarUsed) + + + def test_packageImport(self): + """ + If a dotted name is imported and used, no warning is reported. + """ + self.flakes(''' + import fu.bar + fu.bar + ''') + + + def test_unusedPackageImport(self): + """ + If a dotted name is imported and not used, an unused import warning is + reported. + """ + self.flakes('import fu.bar', m.UnusedImport) + + + def test_duplicateSubmoduleImport(self): + """ + If a submodule of a package is imported twice, an unused import warning + and a redefined while unused warning are reported. + """ + self.flakes(''' + import fu.bar, fu.bar + fu.bar + ''', m.RedefinedWhileUnused) + self.flakes(''' + import fu.bar + import fu.bar + fu.bar + ''', m.RedefinedWhileUnused) + + + def test_differentSubmoduleImport(self): + """ + If two different submodules of a package are imported, no duplicate + import warning is reported for the package. + """ + self.flakes(''' + import fu.bar, fu.baz + fu.bar, fu.baz + ''') + self.flakes(''' + import fu.bar + import fu.baz + fu.bar, fu.baz + ''') + + def test_assignRHSFirst(self): + self.flakes('import fu; fu = fu') + self.flakes('import fu; fu, bar = fu') + self.flakes('import fu; [fu, bar] = fu') + self.flakes('import fu; fu += fu') + + def test_tryingMultipleImports(self): + self.flakes(''' + try: + import fu + except ImportError: + import bar as fu + ''') + test_tryingMultipleImports.todo = '' + + def test_nonGlobalDoesNotRedefine(self): + self.flakes(''' + import fu + def a(): + fu = 3 + return fu + fu + ''') + + def test_functionsRunLater(self): + self.flakes(''' + def a(): + fu + import fu + ''') + + def test_functionNamesAreBoundNow(self): + self.flakes(''' + import fu + def fu(): + fu + fu + ''', m.RedefinedWhileUnused) + + def test_ignoreNonImportRedefinitions(self): + self.flakes('a = 1; a = 2') + + def test_importingForImportError(self): + self.flakes(''' + try: + import fu + except ImportError: + pass + ''') + test_importingForImportError.todo = '' + + def test_importedInClass(self): + '''Imports in class scope can be used through self''' + self.flakes(''' + class c: + import i + def __init__(self): + self.i + ''') + test_importedInClass.todo = 'requires evaluating attribute access' + + def test_futureImport(self): + '''__future__ is special''' + self.flakes('from __future__ import division') + self.flakes(''' + "docstring is allowed before future import" + from __future__ import division + ''') + + def test_futureImportFirst(self): + """ + __future__ imports must come before anything else. + """ + self.flakes(''' + x = 5 + from __future__ import division + ''', m.LateFutureImport) + self.flakes(''' + from foo import bar + from __future__ import division + bar + ''', m.LateFutureImport) + + + +class TestSpecialAll(harness.Test): + """ + Tests for suppression of unused import warnings by C{__all__}. + """ + def test_ignoredInFunction(self): + """ + An C{__all__} definition does not suppress unused import warnings in a + function scope. + """ + self.flakes(''' + def foo(): + import bar + __all__ = ["bar"] + ''', m.UnusedImport, m.UnusedVariable) + + + def test_ignoredInClass(self): + """ + An C{__all__} definition does not suppress unused import warnings in a + class scope. + """ + self.flakes(''' + class foo: + import bar + __all__ = ["bar"] + ''', m.UnusedImport) + + + def test_warningSuppressed(self): + """ + If a name is imported and unused but is named in C{__all__}, no warning + is reported. + """ + self.flakes(''' + import foo + __all__ = ["foo"] + ''') + + + def test_unrecognizable(self): + """ + If C{__all__} is defined in a way that can't be recognized statically, + it is ignored. + """ + self.flakes(''' + import foo + __all__ = ["f" + "oo"] + ''', m.UnusedImport) + self.flakes(''' + import foo + __all__ = [] + ["foo"] + ''', m.UnusedImport) + + + def test_unboundExported(self): + """ + If C{__all__} includes a name which is not bound, a warning is emitted. + """ + self.flakes(''' + __all__ = ["foo"] + ''', m.UndefinedExport) + + # Skip this in __init__.py though, since the rules there are a little + # different. + for filename in ["foo/__init__.py", "__init__.py"]: + self.flakes(''' + __all__ = ["foo"] + ''', filename=filename) + + + def test_usedInGenExp(self): + """ + Using a global in a generator expression results in no warnings. + """ + self.flakes('import fu; (fu for _ in range(1))') + self.flakes('import fu; (1 for _ in range(1) if fu)') + + + def test_redefinedByGenExp(self): + """ + Re-using a global name as the loop variable for a generator + expression results in a redefinition warning. + """ + self.flakes('import fu; (1 for fu in range(1))', m.RedefinedWhileUnused) + + + def test_usedAsDecorator(self): + """ + Using a global name in a decorator statement results in no warnings, + but using an undefined name in a decorator statement results in an + undefined name warning. + """ + self.flakes(''' + from interior import decorate + @decorate + def f(): + return "hello" + ''') + + self.flakes(''' + from interior import decorate + @decorate('value') + def f(): + return "hello" + ''') + + self.flakes(''' + @decorate + def f(): + return "hello" + ''', m.UndefinedName) + + +class Python26Tests(harness.Test): + """ + Tests for checking of syntax which is valid in PYthon 2.6 and newer. + """ + if version_info < (2, 6): + skip = "Python 2.6 required for class decorator tests." + + + def test_usedAsClassDecorator(self): + """ + Using an imported name as a class decorator results in no warnings, + but using an undefined name as a class decorator results in an + undefined name warning. + """ + self.flakes(''' + from interior import decorate + @decorate + class foo: + pass + ''') + + self.flakes(''' + from interior import decorate + @decorate("foo") + class bar: + pass + ''') + + self.flakes(''' + @decorate + class foo: + pass + ''', m.UndefinedName) diff --git a/flake8/test/test_other.py b/flake8/test/test_other.py new file mode 100644 index 0000000..2b7723c --- /dev/null +++ b/flake8/test/test_other.py @@ -0,0 +1,575 @@ +# (c) 2005-2010 Divmod, Inc. +# See LICENSE file for details + +""" +Tests for various Pyflakes behavior. +""" + +from sys import version_info + +from pyflakes import messages as m +from pyflakes.test import harness + + +class Test(harness.Test): + + def test_duplicateArgs(self): + self.flakes('def fu(bar, bar): pass', m.DuplicateArgument) + + def test_localReferencedBeforeAssignment(self): + self.flakes(''' + a = 1 + def f(): + a; a=1 + f() + ''', m.UndefinedName) + test_localReferencedBeforeAssignment.todo = 'this requires finding all assignments in the function body first' + + def test_redefinedFunction(self): + """ + Test that shadowing a function definition with another one raises a + warning. + """ + self.flakes(''' + def a(): pass + def a(): pass + ''', m.RedefinedFunction) + + def test_redefinedClassFunction(self): + """ + Test that shadowing a function definition in a class suite with another + one raises a warning. + """ + self.flakes(''' + class A: + def a(): pass + def a(): pass + ''', m.RedefinedFunction) + + def test_functionDecorator(self): + """ + Test that shadowing a function definition with a decorated version of + that function does not raise a warning. + """ + self.flakes(''' + from somewhere import somedecorator + + def a(): pass + a = somedecorator(a) + ''') + + def test_classFunctionDecorator(self): + """ + Test that shadowing a function definition in a class suite with a + decorated version of that function does not raise a warning. + """ + self.flakes(''' + class A: + def a(): pass + a = classmethod(a) + ''') + + def test_unaryPlus(self): + '''Don't die on unary +''' + self.flakes('+1') + + + def test_undefinedBaseClass(self): + """ + If a name in the base list of a class definition is undefined, a + warning is emitted. + """ + self.flakes(''' + class foo(foo): + pass + ''', m.UndefinedName) + + + def test_classNameUndefinedInClassBody(self): + """ + If a class name is used in the body of that class's definition and + the name is not already defined, a warning is emitted. + """ + self.flakes(''' + class foo: + foo + ''', m.UndefinedName) + + + def test_classNameDefinedPreviously(self): + """ + If a class name is used in the body of that class's definition and + the name was previously defined in some other way, no warning is + emitted. + """ + self.flakes(''' + foo = None + class foo: + foo + ''') + + + def test_comparison(self): + """ + If a defined name is used on either side of any of the six comparison + operators, no warning is emitted. + """ + self.flakes(''' + x = 10 + y = 20 + x < y + x <= y + x == y + x != y + x >= y + x > y + ''') + + + def test_identity(self): + """ + If a deefined name is used on either side of an identity test, no + warning is emitted. + """ + self.flakes(''' + x = 10 + y = 20 + x is y + x is not y + ''') + + + def test_containment(self): + """ + If a defined name is used on either side of a containment test, no + warning is emitted. + """ + self.flakes(''' + x = 10 + y = 20 + x in y + x not in y + ''') + + + def test_loopControl(self): + """ + break and continue statements are supported. + """ + self.flakes(''' + for x in [1, 2]: + break + ''') + self.flakes(''' + for x in [1, 2]: + continue + ''') + + + def test_ellipsis(self): + """ + Ellipsis in a slice is supported. + """ + self.flakes(''' + [1, 2][...] + ''') + + + def test_extendedSlice(self): + """ + Extended slices are supported. + """ + self.flakes(''' + x = 3 + [1, 2][x,:] + ''') + + + +class TestUnusedAssignment(harness.Test): + """ + Tests for warning about unused assignments. + """ + + def test_unusedVariable(self): + """ + Warn when a variable in a function is assigned a value that's never + used. + """ + self.flakes(''' + def a(): + b = 1 + ''', m.UnusedVariable) + + + def test_assignToGlobal(self): + """ + Assigning to a global and then not using that global is perfectly + acceptable. Do not mistake it for an unused local variable. + """ + self.flakes(''' + b = 0 + def a(): + global b + b = 1 + ''') + + + def test_assignToMember(self): + """ + Assigning to a member of another object and then not using that member + variable is perfectly acceptable. Do not mistake it for an unused + local variable. + """ + # XXX: Adding this test didn't generate a failure. Maybe not + # necessary? + self.flakes(''' + class b: + pass + def a(): + b.foo = 1 + ''') + + + def test_assignInForLoop(self): + """ + Don't warn when a variable in a for loop is assigned to but not used. + """ + self.flakes(''' + def f(): + for i in range(10): + pass + ''') + + + def test_assignInListComprehension(self): + """ + Don't warn when a variable in a list comprehension is assigned to but + not used. + """ + self.flakes(''' + def f(): + [None for i in range(10)] + ''') + + + def test_generatorExpression(self): + """ + Don't warn when a variable in a generator expression is assigned to but not used. + """ + self.flakes(''' + def f(): + (None for i in range(10)) + ''') + + + def test_assignmentInsideLoop(self): + """ + Don't warn when a variable assignment occurs lexically after its use. + """ + self.flakes(''' + def f(): + x = None + for i in range(10): + if i > 2: + return x + x = i * 2 + ''') + + + def test_tupleUnpacking(self): + """ + Don't warn when a variable included in tuple unpacking is unused. It's + very common for variables in a tuple unpacking assignment to be unused + in good Python code, so warning will only create false positives. + """ + self.flakes(''' + def f(): + (x, y) = 1, 2 + ''') + + + def test_listUnpacking(self): + """ + Don't warn when a variable included in list unpacking is unused. + """ + self.flakes(''' + def f(): + [x, y] = [1, 2] + ''') + + + def test_closedOver(self): + """ + Don't warn when the assignment is used in an inner function. + """ + self.flakes(''' + def barMaker(): + foo = 5 + def bar(): + return foo + return bar + ''') + + + def test_doubleClosedOver(self): + """ + Don't warn when the assignment is used in an inner function, even if + that inner function itself is in an inner function. + """ + self.flakes(''' + def barMaker(): + foo = 5 + def bar(): + def baz(): + return foo + return bar + ''') + + + +class Python25Test(harness.Test): + """ + Tests for checking of syntax only available in Python 2.5 and newer. + """ + if version_info < (2, 5): + skip = "Python 2.5 required for if-else and with tests" + + def test_ifexp(self): + """ + Test C{foo if bar else baz} statements. + """ + self.flakes("a = 'moo' if True else 'oink'") + self.flakes("a = foo if True else 'oink'", m.UndefinedName) + self.flakes("a = 'moo' if True else bar", m.UndefinedName) + + + def test_withStatementNoNames(self): + """ + No warnings are emitted for using inside or after a nameless C{with} + statement a name defined beforehand. + """ + self.flakes(''' + from __future__ import with_statement + bar = None + with open("foo"): + bar + bar + ''') + + def test_withStatementSingleName(self): + """ + No warnings are emitted for using a name defined by a C{with} statement + within the suite or afterwards. + """ + self.flakes(''' + from __future__ import with_statement + with open('foo') as bar: + bar + bar + ''') + + + def test_withStatementAttributeName(self): + """ + No warnings are emitted for using an attribute as the target of a + C{with} statement. + """ + self.flakes(''' + from __future__ import with_statement + import foo + with open('foo') as foo.bar: + pass + ''') + + + def test_withStatementSubscript(self): + """ + No warnings are emitted for using a subscript as the target of a + C{with} statement. + """ + self.flakes(''' + from __future__ import with_statement + import foo + with open('foo') as foo[0]: + pass + ''') + + + def test_withStatementSubscriptUndefined(self): + """ + An undefined name warning is emitted if the subscript used as the + target of a C{with} statement is not defined. + """ + self.flakes(''' + from __future__ import with_statement + import foo + with open('foo') as foo[bar]: + pass + ''', m.UndefinedName) + + + def test_withStatementTupleNames(self): + """ + No warnings are emitted for using any of the tuple of names defined by + a C{with} statement within the suite or afterwards. + """ + self.flakes(''' + from __future__ import with_statement + with open('foo') as (bar, baz): + bar, baz + bar, baz + ''') + + + def test_withStatementListNames(self): + """ + No warnings are emitted for using any of the list of names defined by a + C{with} statement within the suite or afterwards. + """ + self.flakes(''' + from __future__ import with_statement + with open('foo') as [bar, baz]: + bar, baz + bar, baz + ''') + + + def test_withStatementComplicatedTarget(self): + """ + If the target of a C{with} statement uses any or all of the valid forms + for that part of the grammar (See + U{http://docs.python.org/reference/compound_stmts.html#the-with-statement}), + the names involved are checked both for definedness and any bindings + created are respected in the suite of the statement and afterwards. + """ + self.flakes(''' + from __future__ import with_statement + c = d = e = g = h = i = None + with open('foo') as [(a, b), c[d], e.f, g[h:i]]: + a, b, c, d, e, g, h, i + a, b, c, d, e, g, h, i + ''') + + + def test_withStatementSingleNameUndefined(self): + """ + An undefined name warning is emitted if the name first defined by a + C{with} statement is used before the C{with} statement. + """ + self.flakes(''' + from __future__ import with_statement + bar + with open('foo') as bar: + pass + ''', m.UndefinedName) + + + def test_withStatementTupleNamesUndefined(self): + """ + An undefined name warning is emitted if a name first defined by a the + tuple-unpacking form of the C{with} statement is used before the + C{with} statement. + """ + self.flakes(''' + from __future__ import with_statement + baz + with open('foo') as (bar, baz): + pass + ''', m.UndefinedName) + + + def test_withStatementSingleNameRedefined(self): + """ + A redefined name warning is emitted if a name bound by an import is + rebound by the name defined by a C{with} statement. + """ + self.flakes(''' + from __future__ import with_statement + import bar + with open('foo') as bar: + pass + ''', m.RedefinedWhileUnused) + + + def test_withStatementTupleNamesRedefined(self): + """ + A redefined name warning is emitted if a name bound by an import is + rebound by one of the names defined by the tuple-unpacking form of a + C{with} statement. + """ + self.flakes(''' + from __future__ import with_statement + import bar + with open('foo') as (bar, baz): + pass + ''', m.RedefinedWhileUnused) + + + def test_withStatementUndefinedInside(self): + """ + An undefined name warning is emitted if a name is used inside the + body of a C{with} statement without first being bound. + """ + self.flakes(''' + from __future__ import with_statement + with open('foo') as bar: + baz + ''', m.UndefinedName) + + + def test_withStatementNameDefinedInBody(self): + """ + A name defined in the body of a C{with} statement can be used after + the body ends without warning. + """ + self.flakes(''' + from __future__ import with_statement + with open('foo') as bar: + baz = 10 + baz + ''') + + + def test_withStatementUndefinedInExpression(self): + """ + An undefined name warning is emitted if a name in the I{test} + expression of a C{with} statement is undefined. + """ + self.flakes(''' + from __future__ import with_statement + with bar as baz: + pass + ''', m.UndefinedName) + + self.flakes(''' + from __future__ import with_statement + with bar as bar: + pass + ''', m.UndefinedName) + + + +class Python27Test(harness.Test): + """ + Tests for checking of syntax only available in Python 2.7 and newer. + """ + if version_info < (2, 7): + skip = "Python 2.7 required for dict/set comprehension tests" + + def test_dictComprehension(self): + """ + Dict comprehensions are properly handled. + """ + self.flakes(''' + a = {1: x for x in range(10)} + ''') + + def test_setComprehensionAndLiteral(self): + """ + Set comprehensions are properly handled. + """ + self.flakes(''' + a = {1, 2, 3} + b = {x for x in range(10)} + ''') diff --git a/flake8/test/test_script.py b/flake8/test/test_script.py new file mode 100644 index 0000000..233e59e --- /dev/null +++ b/flake8/test/test_script.py @@ -0,0 +1,185 @@ + +""" +Tests for L{pyflakes.scripts.pyflakes}. +""" + +import sys +from StringIO import StringIO + +from twisted.python.filepath import FilePath +from twisted.trial.unittest import TestCase + +from pyflakes.scripts.pyflakes import checkPath + +def withStderrTo(stderr, f): + """ + Call C{f} with C{sys.stderr} redirected to C{stderr}. + """ + (outer, sys.stderr) = (sys.stderr, stderr) + try: + return f() + finally: + sys.stderr = outer + + + +class CheckTests(TestCase): + """ + Tests for L{check} and L{checkPath} which check a file for flakes. + """ + def test_missingTrailingNewline(self): + """ + Source which doesn't end with a newline shouldn't cause any + exception to be raised nor an error indicator to be returned by + L{check}. + """ + fName = self.mktemp() + FilePath(fName).setContent("def foo():\n\tpass\n\t") + self.assertFalse(checkPath(fName)) + + + def test_checkPathNonExisting(self): + """ + L{checkPath} handles non-existing files. + """ + err = StringIO() + count = withStderrTo(err, lambda: checkPath('extremo')) + self.assertEquals(err.getvalue(), 'extremo: No such file or directory\n') + self.assertEquals(count, 1) + + + def test_multilineSyntaxError(self): + """ + Source which includes a syntax error which results in the raised + L{SyntaxError.text} containing multiple lines of source are reported + with only the last line of that source. + """ + source = """\ +def foo(): + ''' + +def bar(): + pass + +def baz(): + '''quux''' +""" + + # Sanity check - SyntaxError.text should be multiple lines, if it + # isn't, something this test was unprepared for has happened. + def evaluate(source): + exec source + exc = self.assertRaises(SyntaxError, evaluate, source) + self.assertTrue(exc.text.count('\n') > 1) + + sourcePath = FilePath(self.mktemp()) + sourcePath.setContent(source) + err = StringIO() + count = withStderrTo(err, lambda: checkPath(sourcePath.path)) + self.assertEqual(count, 1) + + self.assertEqual( + err.getvalue(), + """\ +%s:8: invalid syntax + '''quux''' + ^ +""" % (sourcePath.path,)) + + + def test_eofSyntaxError(self): + """ + The error reported for source files which end prematurely causing a + syntax error reflects the cause for the syntax error. + """ + source = "def foo(" + sourcePath = FilePath(self.mktemp()) + sourcePath.setContent(source) + err = StringIO() + count = withStderrTo(err, lambda: checkPath(sourcePath.path)) + self.assertEqual(count, 1) + self.assertEqual( + err.getvalue(), + """\ +%s:1: unexpected EOF while parsing +def foo( + ^ +""" % (sourcePath.path,)) + + + def test_nonDefaultFollowsDefaultSyntaxError(self): + """ + Source which has a non-default argument following a default argument + should include the line number of the syntax error. However these + exceptions do not include an offset. + """ + source = """\ +def foo(bar=baz, bax): + pass +""" + sourcePath = FilePath(self.mktemp()) + sourcePath.setContent(source) + err = StringIO() + count = withStderrTo(err, lambda: checkPath(sourcePath.path)) + self.assertEqual(count, 1) + self.assertEqual( + err.getvalue(), + """\ +%s:1: non-default argument follows default argument +def foo(bar=baz, bax): +""" % (sourcePath.path,)) + + + def test_nonKeywordAfterKeywordSyntaxError(self): + """ + Source which has a non-keyword argument after a keyword argument should + include the line number of the syntax error. However these exceptions + do not include an offset. + """ + source = """\ +foo(bar=baz, bax) +""" + sourcePath = FilePath(self.mktemp()) + sourcePath.setContent(source) + err = StringIO() + count = withStderrTo(err, lambda: checkPath(sourcePath.path)) + self.assertEqual(count, 1) + self.assertEqual( + err.getvalue(), + """\ +%s:1: non-keyword arg after keyword arg +foo(bar=baz, bax) +""" % (sourcePath.path,)) + + + def test_permissionDenied(self): + """ + If the a source file is not readable, this is reported on standard + error. + """ + sourcePath = FilePath(self.mktemp()) + sourcePath.setContent('') + sourcePath.chmod(0) + err = StringIO() + count = withStderrTo(err, lambda: checkPath(sourcePath.path)) + self.assertEquals(count, 1) + self.assertEquals( + err.getvalue(), "%s: Permission denied\n" % (sourcePath.path,)) + + + def test_misencodedFile(self): + """ + If a source file contains bytes which cannot be decoded, this is + reported on stderr. + """ + source = u"""\ +# coding: ascii +x = "\N{SNOWMAN}" +""".encode('utf-8') + sourcePath = FilePath(self.mktemp()) + sourcePath.setContent(source) + err = StringIO() + count = withStderrTo(err, lambda: checkPath(sourcePath.path)) + self.assertEquals(count, 1) + self.assertEquals( + err.getvalue(), "%s: problem decoding source\n" % (sourcePath.path,)) diff --git a/flake8/test/test_undefined_names.py b/flake8/test/test_undefined_names.py new file mode 100644 index 0000000..309f0b9 --- /dev/null +++ b/flake8/test/test_undefined_names.py @@ -0,0 +1,265 @@ + +from _ast import PyCF_ONLY_AST + +from twisted.trial.unittest import TestCase + +from pyflakes import messages as m, checker +from pyflakes.test import harness + + +class Test(harness.Test): + def test_undefined(self): + self.flakes('bar', m.UndefinedName) + + def test_definedInListComp(self): + self.flakes('[a for a in range(10) if a]') + + + def test_functionsNeedGlobalScope(self): + self.flakes(''' + class a: + def b(): + fu + fu = 1 + ''') + + def test_builtins(self): + self.flakes('range(10)') + + + def test_magicGlobalsFile(self): + """ + Use of the C{__file__} magic global should not emit an undefined name + warning. + """ + self.flakes('__file__') + + + def test_magicGlobalsBuiltins(self): + """ + Use of the C{__builtins__} magic global should not emit an undefined + name warning. + """ + self.flakes('__builtins__') + + + def test_magicGlobalsName(self): + """ + Use of the C{__name__} magic global should not emit an undefined name + warning. + """ + self.flakes('__name__') + + + def test_magicGlobalsPath(self): + """ + Use of the C{__path__} magic global should not emit an undefined name + warning, if you refer to it from a file called __init__.py. + """ + self.flakes('__path__', m.UndefinedName) + self.flakes('__path__', filename='package/__init__.py') + + + def test_globalImportStar(self): + '''Can't find undefined names with import *''' + self.flakes('from fu import *; bar', m.ImportStarUsed) + + def test_localImportStar(self): + '''A local import * still allows undefined names to be found in upper scopes''' + self.flakes(''' + def a(): + from fu import * + bar + ''', m.ImportStarUsed, m.UndefinedName) + + def test_unpackedParameter(self): + '''Unpacked function parameters create bindings''' + self.flakes(''' + def a((bar, baz)): + bar; baz + ''') + + def test_definedByGlobal(self): + '''"global" can make an otherwise undefined name in another function defined''' + self.flakes(''' + def a(): global fu; fu = 1 + def b(): fu + ''') + test_definedByGlobal.todo = '' + + def test_globalInGlobalScope(self): + """ + A global statement in the global scope is ignored. + """ + self.flakes(''' + global x + def foo(): + print x + ''', m.UndefinedName) + + def test_del(self): + '''del deletes bindings''' + self.flakes('a = 1; del a; a', m.UndefinedName) + + def test_delGlobal(self): + '''del a global binding from a function''' + self.flakes(''' + a = 1 + def f(): + global a + del a + a + ''') + + def test_delUndefined(self): + '''del an undefined name''' + self.flakes('del a', m.UndefinedName) + + def test_globalFromNestedScope(self): + '''global names are available from nested scopes''' + self.flakes(''' + a = 1 + def b(): + def c(): + a + ''') + + def test_laterRedefinedGlobalFromNestedScope(self): + """ + Test that referencing a local name that shadows a global, before it is + defined, generates a warning. + """ + self.flakes(''' + a = 1 + def fun(): + a + a = 2 + return a + ''', m.UndefinedLocal) + + def test_laterRedefinedGlobalFromNestedScope2(self): + """ + Test that referencing a local name in a nested scope that shadows a + global declared in an enclosing scope, before it is defined, generates + a warning. + """ + self.flakes(''' + a = 1 + def fun(): + global a + def fun2(): + a + a = 2 + return a + ''', m.UndefinedLocal) + + + def test_intermediateClassScopeIgnored(self): + """ + If a name defined in an enclosing scope is shadowed by a local variable + and the name is used locally before it is bound, an unbound local + warning is emitted, even if there is a class scope between the enclosing + scope and the local scope. + """ + self.flakes(''' + def f(): + x = 1 + class g: + def h(self): + a = x + x = None + print x, a + print x + ''', m.UndefinedLocal) + + + def test_doubleNestingReportsClosestName(self): + """ + Test that referencing a local name in a nested scope that shadows a + variable declared in two different outer scopes before it is defined + in the innermost scope generates an UnboundLocal warning which + refers to the nearest shadowed name. + """ + exc = self.flakes(''' + def a(): + x = 1 + def b(): + x = 2 # line 5 + def c(): + x + x = 3 + return x + return x + return x + ''', m.UndefinedLocal).messages[0] + self.assertEqual(exc.message_args, ('x', 5)) + + + def test_laterRedefinedGlobalFromNestedScope3(self): + """ + Test that referencing a local name in a nested scope that shadows a + global, before it is defined, generates a warning. + """ + self.flakes(''' + def fun(): + a = 1 + def fun2(): + a + a = 1 + return a + return a + ''', m.UndefinedLocal) + + def test_nestedClass(self): + '''nested classes can access enclosing scope''' + self.flakes(''' + def f(foo): + class C: + bar = foo + def f(self): + return foo + return C() + + f(123).f() + ''') + + def test_badNestedClass(self): + '''free variables in nested classes must bind at class creation''' + self.flakes(''' + def f(): + class C: + bar = foo + foo = 456 + return foo + f() + ''', m.UndefinedName) + + def test_definedAsStarArgs(self): + '''star and double-star arg names are defined''' + self.flakes(''' + def f(a, *b, **c): + print a, b, c + ''') + + def test_definedInGenExp(self): + """ + Using the loop variable of a generator expression results in no + warnings. + """ + self.flakes('(a for a in xrange(10) if a)') + + + +class NameTests(TestCase): + """ + Tests for some extra cases of name handling. + """ + def test_impossibleContext(self): + """ + A Name node with an unrecognized context results in a RuntimeError being + raised. + """ + tree = compile("x = 10", "", "exec", PyCF_ONLY_AST) + # Make it into something unrecognizable. + tree.body[0].targets[0].ctx = object() + self.assertRaises(RuntimeError, checker.Checker, tree) diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..4e73ccd --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +from distutils.core import setup + +setup( + name="flake8", + license="MIT", + version="0.1", + description="code checking", + author="Tarek Ziade", + url="http://bitbucket.org/tarek/flake8", + packages=["flake8", "flake8.scripts", "flake8.test"], + scripts=["bin/flake8"], + long_description=README, + classifiers=[ + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Software Development", + "Topic :: Utilities", + ])