From 31bcca53f940561a48e63fed2f348e334f52a00a Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 26 Dec 2012 17:59:23 -0500 Subject: [PATCH 01/20] Start the refactor. --- flake8/pep8.py | 8 +------- flake8/run.py | 35 +++++++++++++++++++++++++---------- setup.py | 1 + 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/flake8/pep8.py b/flake8/pep8.py index 92b7dc3..78cf6f8 100755 --- a/flake8/pep8.py +++ b/flake8/pep8.py @@ -92,8 +92,6 @@ 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 and \t for tab. """ -from flake8.pyflakes import __version__ as pyflakes_version -from flake8 import __version__ as flake8_version __version__ = '1.3.5a' @@ -1853,11 +1851,7 @@ def process_options(arglist=None, parse_argv=False, config_file=None): if config_file is True: config_file = DEFAULT_CONFIG - version = '%s (pyflakes: %s, pep8: %s)' % \ - (flake8_version, pyflakes_version, __version__) - - - parser = OptionParser(version=version, + parser = OptionParser(version=__version__, usage="%prog [options] input ...") parser.config_options = [ 'builtins', 'count', 'exclude', 'filename', 'format', 'ignore', diff --git a/flake8/run.py b/flake8/run.py index 6cd3338..85d72b9 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -5,17 +5,17 @@ Implementation of the command-line I{flake8} tool. import sys import os import os.path +import optparse from subprocess import PIPE, Popen import select -try: - from StringIO import StringIO -except ImportError: - from io import StringIO # NOQA -from flake8.util import skip_file from flake8 import pep8 -from flake8 import pyflakes from flake8 import mccabe +from flake8.util import skip_file +from flake8 import __version__ as flake8_version +from flakey import __version__ as flakey_version +from flakey import checker +from flakey.scripts import flakey pep8style = None @@ -23,7 +23,8 @@ pep8style = None def check_file(path, ignore=(), complexity=-1): if pep8style.excluded(path): return 0 - warnings = pyflakes.checkPath(path, ignore) + #warnings = flakey.checkPath(path, ignore) + warnings = flakey.checkPath(path) warnings += pep8style.input_file(path) if complexity > -1: warnings += mccabe.get_module_complexity(path, complexity) @@ -31,7 +32,7 @@ def check_file(path, ignore=(), complexity=-1): def check_code(code, ignore=(), complexity=-1): - warnings = pyflakes.check(code, ignore, 'stdin') + warnings = flakey.check(code, ignore, 'stdin') warnings += pep8style.input_file(None, lines=code.split('\n')) if complexity > -1: warnings += mccabe.get_code_complexity(code, complexity) @@ -66,8 +67,22 @@ def read_stdin(): return sys.stdin.read() +def version(option, opt, value, parser): + parser.print_usage() + parser.print_version() + sys.exit(0) + + def main(): global pep8style + parser = optparse.OptionParser('%prog [options]', version=version) + parser.version = '{0} (pep8: {1}, flakey: {2})'.format( + flake8_version, pep8.__version__, flakey_version) + parser.remove_option('--version') + parser.add_option('-v', '--version', action='callback', + callback=version, + help='Print the version info for flake8') + parser.parse_args() pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) options = pep8style.options complexity = options.max_complexity @@ -76,8 +91,8 @@ def main(): stdin = None if builtins: - orig_builtins = set(pyflakes._MAGIC_GLOBALS) - pyflakes._MAGIC_GLOBALS = orig_builtins | builtins + orig_builtins = set(checker._MAGIC_GLOBALS) + checker._MAGIC_GLOBALS = orig_builtins | builtins if pep8style.paths and options.filename is not None: for path in _get_python_files(pep8style.paths): diff --git a/setup.py b/setup.py index 0d85ec6..3fa5bc0 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ setup( url="http://bitbucket.org/tarek/flake8", packages=["flake8", "flake8.tests"], scripts=scripts, + requires=['flakey'], long_description=README, classifiers=[ "Environment :: Console", From c17aeb3848866eb2a587a9150fa793411ca109c9 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Fri, 28 Dec 2012 23:19:56 -0500 Subject: [PATCH 02/20] A working version but still using a local copy of pep8 --- flake8/pep8.py | 1 - flake8/pyflakes.py | 723 --------------------------------------------- flake8/run.py | 48 +-- setup.py | 2 +- 4 files changed, 32 insertions(+), 742 deletions(-) delete mode 100644 flake8/pyflakes.py diff --git a/flake8/pep8.py b/flake8/pep8.py index 78cf6f8..c645966 100755 --- a/flake8/pep8.py +++ b/flake8/pep8.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# flake8: noqa # pep8.py - Check Python source code formatting, according to PEP 8 # Copyright (C) 2006-2009 Johann C. Rocholl # Copyright (C) 2009-2012 Florent Xicluna diff --git a/flake8/pyflakes.py b/flake8/pyflakes.py deleted file mode 100644 index 7539380..0000000 --- a/flake8/pyflakes.py +++ /dev/null @@ -1,723 +0,0 @@ -# -*- test-case-name: pyflakes -*- -# (c) 2005-2010 Divmod, Inc. -# See LICENSE file for details - -try: - import __builtin__ # NOQA -except ImportError: - import builtins as __builtin__ # NOQA - -import os.path -import _ast -import sys - -from flake8 import messages -from flake8.util import skip_warning - -__version__ = '0.5.0' - -# 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.values(): - 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 = TRY = \ - WITHITEM = 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 = KEYWORD = handleChildren - - def EXCEPTHANDLER(self, node): - - if node.name is not None: - if isinstance(node.name, str): - name = node.name - elif hasattr(node.name, 'elts'): - names = [e.id for e in node.name.elts] - name = '({0})'.format(', '.join(names)) - else: - name = node.name.id - self.addBinding(node.lineno, Assignment(name, node)) - - def runException(): - for stmt in iter_child_nodes(node): - self.handleNode(stmt, node) - - self.deferFunction(runException) - - 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: - try: - id_ = arg.id - except AttributeError: - id_ = arg.arg - - if id_ in args: - self.report(messages.DuplicateArgument, - node.lineno, id_) - args.append(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.items(): - 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) - - -def checkPath(filename, ignore=[]): - """ - Check the given path, printing out any warnings detected. - - @return: the number of warnings printed - """ - try: - return check(open(filename, 'U').read() + '\n', ignore, filename) - except IOError: - msg = sys.exc_info()[1] - sys.stderr.write("%s: %s\n" % (filename, msg.args[1])) - return 1 - - -def check(codeString, ignore, filename='stdin'): - """ - 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 = sys.exc_info()[1] - 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. - sys.stderr.write("%s: problem decoding source\n" % (filename)) - else: - line = text.splitlines()[-1] - - if offset is not None: - offset = offset - (len(text) - len(line)) - - sys.stderr.write('%s:%d: %s\n' % (filename, lineno, msg)) - sys.stderr.write(line + '\n') - - if offset is not None: - sys.stderr.write(" " * offset + "^\n") - - return 1 - else: - # Okay, it's syntactically valid. Now check it. - w = Checker(tree, filename) - sorting = [(msg.lineno, msg) for msg in w.messages] - sorting.sort() - w.messages = [msg for index, msg in sorting] - valid_warnings = 0 - - for warning in w.messages: - if skip_warning(warning, ignore): - continue - print(warning) - valid_warnings += 1 - - return valid_warnings diff --git a/flake8/run.py b/flake8/run.py index 85d72b9..1b2c495 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -12,10 +12,8 @@ import select from flake8 import pep8 from flake8 import mccabe from flake8.util import skip_file -from flake8 import __version__ as flake8_version -from flakey import __version__ as flakey_version -from flakey import checker -from flakey.scripts import flakey +from flake8 import __version__ +import flakey pep8style = None @@ -23,8 +21,8 @@ pep8style = None def check_file(path, ignore=(), complexity=-1): if pep8style.excluded(path): return 0 - #warnings = flakey.checkPath(path, ignore) - warnings = flakey.checkPath(path) + warning = flakey.checkPath(path) + warnings = flakey.print_messages(warning, ignore=ignore) warnings += pep8style.input_file(path) if complexity > -1: warnings += mccabe.get_module_complexity(path, complexity) @@ -32,7 +30,8 @@ def check_file(path, ignore=(), complexity=-1): def check_code(code, ignore=(), complexity=-1): - warnings = flakey.check(code, ignore, 'stdin') + warning = flakey.check(code, 'stdin') + warnings = flakey.print_messages(warning, ignore=ignore) warnings += pep8style.input_file(None, lines=code.split('\n')) if complexity > -1: warnings += mccabe.get_code_complexity(code, complexity) @@ -75,40 +74,55 @@ def version(option, opt, value, parser): def main(): global pep8style + + # Create our own parser parser = optparse.OptionParser('%prog [options]', version=version) parser.version = '{0} (pep8: {1}, flakey: {2})'.format( - flake8_version, pep8.__version__, flakey_version) + __version__, pep8.__version__, flakey.__version__) parser.remove_option('--version') - parser.add_option('-v', '--version', action='callback', + # don't overlap with pep8's verbose option + parser.add_option('--builtins', default='', dest='builtins', + help="append builtin functions to flakey's " + "_MAGIC_BUILTINS") + parser.add_option('--ignore', default='', + help='skip errors and warnings (e.g. E4,W)') + parser.add_option('--exit-zero', action='store_true', default=False, + help='Exit with status 0 even if there are errors') + parser.add_option('--max-complexity', default=-1, action='store', + type='int', help='McCabe complexity threshold') + parser.add_option('-V', '--version', action='callback', callback=version, help='Print the version info for flake8') - parser.parse_args() + # parse our flags + opts, args = parser.parse_args() + pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) options = pep8style.options complexity = options.max_complexity - builtins = set(options.builtins) + builtins = set(opts.builtins.split(',')) warnings = 0 stdin = None if builtins: - orig_builtins = set(checker._MAGIC_GLOBALS) - checker._MAGIC_GLOBALS = orig_builtins | builtins + orig_builtins = set(flakey.checker._MAGIC_GLOBALS) + flakey.checker._MAGIC_GLOBALS = orig_builtins | builtins if pep8style.paths and options.filename is not None: for path in _get_python_files(pep8style.paths): if path == '-': if stdin is None: stdin = read_stdin() - warnings += check_code(stdin, options.ignore, complexity) + warnings += check_code(stdin, opts.ignore, complexity) else: - warnings += check_file(path, options.ignore, complexity) + warnings += check_file(path, opts.ignore, complexity) else: stdin = read_stdin() - warnings += check_code(stdin, options.ignore, complexity) + warnings += check_code(stdin, opts.ignore, complexity) if options.exit_zero: raise SystemExit(0) - raise SystemExit(warnings > 0) + + raise SystemExit(warnings) def _get_files(repo, **kwargs): diff --git a/setup.py b/setup.py index 3fa5bc0..14d0b89 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup( url="http://bitbucket.org/tarek/flake8", packages=["flake8", "flake8.tests"], scripts=scripts, - requires=['flakey'], + requires=["flakey>=2.0", "pep8"], long_description=README, classifiers=[ "Environment :: Console", From b2135a0e0e862f5b2ec014c71c6a138558df8fe9 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 29 Dec 2012 22:00:22 -0500 Subject: [PATCH 03/20] Tentative independence from vendored pep8 I'm fairly certain we no longer need the copy we had of pep8. --- flake8/__init__.py | 1 - flake8/messages.py | 113 ------------------------------------ flake8/run.py | 26 +++++---- flake8/tests/test_flakes.py | 15 +++-- setup.py | 16 ++--- 5 files changed, 35 insertions(+), 136 deletions(-) delete mode 100644 flake8/messages.py diff --git a/flake8/__init__.py b/flake8/__init__.py index 6660609..2a9b184 100644 --- a/flake8/__init__.py +++ b/flake8/__init__.py @@ -1,3 +1,2 @@ -# __version__ = '1.7.0' diff --git a/flake8/messages.py b/flake8/messages.py deleted file mode 100644 index be63949..0000000 --- a/flake8/messages.py +++ /dev/null @@ -1,113 +0,0 @@ -# (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) - - def __lt__(self, other): - if self.filename != other.filename: - return self.filename < other.filename - return self.lineno < other.lineno - - -class UnusedImport(Message): - message = 'W402 %r imported but unused' - - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) - self.message_args = (name,) - - -class RedefinedWhileUnused(Message): - message = 'W801 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 = 'W403 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 = "W404 '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 = 'W802 undefined name %r' - - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) - self.message_args = (name,) - - -class UndefinedExport(Message): - message = 'W803 undefined name %r in __all__' - - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) - self.message_args = (name,) - - -class UndefinedLocal(Message): - message = "W804 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 = 'W805 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 = 'W806 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 = 'W405 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 = 'W806 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/run.py b/flake8/run.py index 1b2c495..9a6cf2f 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -9,10 +9,10 @@ import optparse from subprocess import PIPE, Popen import select -from flake8 import pep8 from flake8 import mccabe from flake8.util import skip_file from flake8 import __version__ +import pep8 import flakey pep8style = None @@ -66,14 +66,13 @@ def read_stdin(): return sys.stdin.read() -def version(option, opt, value, parser): - parser.print_usage() - parser.print_version() - sys.exit(0) +def get_parser(): + """Create a custom OptionParser""" - -def main(): - global pep8style + def version(option, opt, value, parser): + parser.print_usage() + parser.print_version() + sys.exit(0) # Create our own parser parser = optparse.OptionParser('%prog [options]', version=version) @@ -93,6 +92,13 @@ def main(): parser.add_option('-V', '--version', action='callback', callback=version, help='Print the version info for flake8') + return parser + + +def main(): + global pep8style + + parser = get_parser() # parse our flags opts, args = parser.parse_args() @@ -165,8 +171,8 @@ def _initpep8(): global pep8style if pep8style is None: pep8style = pep8.StyleGuide(config_file=True) - #pep8style.options.physical_checks = pep8.find_checks('physical_line') - #pep8style.options.logical_checks = pep8.find_checks('logical_line') + pep8style.options.physical_checks = pep8.find_checks('physical_line') + pep8style.options.logical_checks = pep8.find_checks('logical_line') pep8style.options.counters = dict.fromkeys(pep8.BENCHMARK_KEYS, 0) pep8style.options.messages = {} pep8style.options.max_line_length = 79 diff --git a/flake8/tests/test_flakes.py b/flake8/tests/test_flakes.py index c1e2514..230b62a 100644 --- a/flake8/tests/test_flakes.py +++ b/flake8/tests/test_flakes.py @@ -1,5 +1,5 @@ from unittest import TestCase -from flake8.pyflakes import check +from flakey import check, print_messages code = """ @@ -49,11 +49,16 @@ class TestFlake(TestCase): def test_exception(self): for c in (code, code2, code3): - warnings = check(code) - self.assertEqual(warnings, 0, code) + warnings = check(code, '(stdin)') + warnings = print_messages(warnings) + self.assertEqual(warnings, 0) def test_from_import_exception_in_scope(self): - self.assertEqual(check(code_from_import_exception), 0) + warnings = check(code_from_import_exception, '(stdin)') + warnings = print_messages(warnings) + self.assertEqual(warnings, 0) def test_import_exception_in_scope(self): - self.assertEqual(check(code_import_exception), 0) + warnings = check(code_import_exception, '(stdin)') + warnings = print_messages(warnings) + self.assertEqual(warnings, 0) diff --git a/setup.py b/setup.py index 14d0b89..0cb9cee 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import sys import os ispy3 = sys.version_info[0] == 3 -iswin = os.name == 'nt' +iswin = os.name == 'nt' kwargs = {} scripts = ["flake8/flake8"] @@ -14,9 +14,10 @@ else: try: from setuptools import setup # NOQA kwargs = { - 'entry_points': - {'distutils.commands': ['flake8 = flake8.run:Flake8Command'], - 'console_scripts': ['flake8 = flake8.run:main']}, + 'entry_points': { + 'distutils.commands': ['flake8 = flake8.run:Flake8Command'], + 'console_scripts': ['flake8 = flake8.run:main'] + }, 'tests_require': ['nose'], 'test_suite': 'nose.collector', } @@ -39,7 +40,7 @@ setup( url="http://bitbucket.org/tarek/flake8", packages=["flake8", "flake8.tests"], scripts=scripts, - requires=["flakey>=2.0", "pep8"], + requires=["flakey (>=2.0)", "pep8 (>=1.4)"], long_description=README, classifiers=[ "Environment :: Console", @@ -48,5 +49,6 @@ setup( "Programming Language :: Python", "Topic :: Software Development", "Topic :: Utilities", - ], - **kwargs) + ], + **kwargs +) From 5342830f80726622f9ff91ff0c3a0b99fb2f6f5b Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 29 Dec 2012 22:01:00 -0500 Subject: [PATCH 04/20] Forgot to remove pep8.py --- flake8/pep8.py | 1989 ------------------------------------------------ 1 file changed, 1989 deletions(-) delete mode 100755 flake8/pep8.py diff --git a/flake8/pep8.py b/flake8/pep8.py deleted file mode 100755 index c645966..0000000 --- a/flake8/pep8.py +++ /dev/null @@ -1,1989 +0,0 @@ -#!/usr/bin/env python -# pep8.py - Check Python source code formatting, according to PEP 8 -# Copyright (C) 2006-2009 Johann C. Rocholl -# Copyright (C) 2009-2012 Florent Xicluna -# -# 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. - -r""" -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 -900 syntax error - -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 and \t for tab. -""" - -__version__ = '1.3.5a' - -import os -import sys -import re -import time -import inspect -import keyword -import tokenize -from optparse import OptionParser -from fnmatch import fnmatch -try: - from configparser import RawConfigParser - from io import TextIOWrapper -except ImportError: - from ConfigParser import RawConfigParser - -DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git' -DEFAULT_IGNORE = 'E226,E24' -if sys.platform == 'win32': - DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') -else: - DEFAULT_CONFIG = os.path.join(os.getenv('XDG_CONFIG_HOME') or - os.path.expanduser('~/.config'), 'pep8') -MAX_LINE_LENGTH = 79 -REPORT_FORMAT = { - 'default': '%(path)s:%(row)d:%(col)d: %(code)s %(text)s', - 'pylint': '%(path)s:%(row)d: [%(code)s] %(text)s', -} - - -SINGLETONS = frozenset(['False', 'None', 'True']) -KEYWORDS = frozenset(keyword.kwlist + ['print']) - SINGLETONS -BINARY_OPERATORS = frozenset([ - '**=', '*=', '+=', '-=', '!=', '<>', - '%=', '^=', '&=', '|=', '==', '/=', '//=', '<=', '>=', '<<=', '>>=', - '%', '^', '&', '|', '=', '/', '//', '<', '>', '<<']) -UNARY_OPERATORS = frozenset(['>>', '**', '*', '+', '-']) -OPERATORS = BINARY_OPERATORS | UNARY_OPERATORS -WS_OPTIONAL_OPERATORS = frozenset(['**', '*', '/', '//', '+', '-']) -WS_NEEDED_OPERATORS = frozenset([ - '**=', '*=', '/=', '//=', '+=', '-=', '!=', '<>', - '%=', '^=', '&=', '|=', '==', '<=', '>=', '<<=', '>>=', - '%', '^', '&', '|', '=', '<', '>', '<<']) -WHITESPACE = frozenset(' \t') -SKIP_TOKENS = frozenset([tokenize.COMMENT, tokenize.NL, tokenize.NEWLINE, - tokenize.INDENT, tokenize.DEDENT]) -BENCHMARK_KEYS = ['directories', 'files', 'logical lines', 'physical lines'] - -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}') -DOCSTRING_REGEX = re.compile(r'u?r?["\']') -EXTRANEOUS_WHITESPACE_REGEX = re.compile(r'[[({] | []}),;:]') -WHITESPACE_AFTER_COMMA_REGEX = re.compile(r'[,;:]\s*(?: |\t)') -COMPARE_SINGLETON_REGEX = re.compile(r'([=!]=)\s*(None|False|True)') -COMPARE_TYPE_REGEX = re.compile(r'([=!]=|is|is\s+not)\s*type(?:s\.(\w+)Type' - r'|\(\s*(\(\s*\)|[^)]*[^ )])\s*\))') -KEYWORD_REGEX = re.compile(r'(?:[^\s]|\b)(\s*)\b(?:%s)\b(\s*)' % - r'|'.join(KEYWORDS)) -OPERATOR_REGEX = re.compile(r'(?:[^,\s])(\s*)(?:[-+*/|!<=>%&^]+)(\s*)') -LAMBDA_REGEX = re.compile(r'\blambda\b') -HUNK_REGEX = re.compile(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$') - -# Work around Python < 2.6 behaviour, which does not generate NL after -# a comment which is on a line by itself. -COMMENT_WITH_NL = tokenize.generate_tokens(['#\n'].pop).send(None)[1] == '#\n' - - -############################################################################## -# 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 '\t' in indent: - return indent.index('\t'), "W191 indentation contains tabs" - - -def trailing_whitespace(physical_line): - r""" - JCR: Trailing whitespace is superfluous. - FBM: Except when it occurs as part of a blank line (i.e. the line is - nothing but whitespace). According to Python docs[1] a line with only - whitespace is considered a blank line, and is to be ignored. However, - matching a blank line to its indentation level avoids mistakenly - terminating a multi-line statement (e.g. class declaration) when - pasting code into the standard Python interpreter. - - [1] http://docs.python.org/reference/lexical_analysis.html#blank-lines - - The warning returned varies on whether the line itself is blank, for easier - filtering for those who want to indent their blank lines. - - Okay: spam(1)\n# - W291: spam(1) \n# - W293: class Foo(object):\n \n bang = 12 - """ - 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(' \t\v') - if physical_line != stripped: - if stripped: - return len(stripped), "W291 trailing whitespace" - else: - return 0, "W293 blank line contains 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 not physical_line.rstrip() 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. - - Reports warning W292. - """ - if physical_line.rstrip() == physical_line: - return len(physical_line), "W292 no newline at end of file" - - -def maximum_line_length(physical_line, max_line_length): - """ - 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. - - Reports error E501. - """ - line = physical_line.rstrip() - length = len(line) - if length > max_line_length: - if line.strip().lower().endswith('# nopep8'): - return - if hasattr(line, 'decode'): # Python 2 - # The line could contain multi-byte characters - try: - length = len(line.decode('utf-8')) - except UnicodeError: - pass - if length > max_line_length: - return (max_line_length, "E501 line too long " - "(%d > %d characters)" % (length, max_line_length)) - - -############################################################################## -# Plugins (check functions) for logical lines -############################################################################## - - -def blank_lines(logical_line, blank_lines, indent_level, line_number, - previous_logical, previous_indent_level): - 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 - if previous_logical.startswith('@'): - if blank_lines: - yield 0, "E304 blank lines found after function decorator" - elif blank_lines > 2 or (indent_level and blank_lines == 2): - yield 0, "E303 too many blank lines (%d)" % blank_lines - elif logical_line.startswith(('def ', 'class ', '@')): - if indent_level: - if not (blank_lines or previous_indent_level < indent_level or - DOCSTRING_REGEX.match(previous_logical)): - yield 0, "E301 expected 1 blank line, found 0" - elif blank_lines != 2: - yield 0, "E302 expected 2 blank lines, found %d" % 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 match in EXTRANEOUS_WHITESPACE_REGEX.finditer(line): - text = match.group() - char = text.strip() - found = match.start() - if text == char + ' ': - # assert char in '([{' - yield found + 1, "E201 whitespace after '%s'" % char - elif line[found - 1] != ',': - code = ('E202' if char in '}])' else 'E203') # if char in ',;:' - yield found, "%s whitespace before '%s'" % (code, char) - - -def whitespace_around_keywords(logical_line): - r""" - Avoid extraneous whitespace around keywords. - - Okay: True and False - E271: True and False - E272: True and False - E273: True and\tFalse - E274: True\tand False - """ - for match in KEYWORD_REGEX.finditer(logical_line): - before, after = match.groups() - - if '\t' in before: - yield match.start(1), "E274 tab before keyword" - elif len(before) > 1: - yield match.start(1), "E272 multiple spaces before keyword" - - if '\t' in after: - yield match.start(2), "E273 tab after keyword" - elif len(after) > 1: - yield match.start(2), "E271 multiple spaces after keyword" - - -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) - E231: [{'a':'b'}] - """ - 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(']') and \ - before.rfind('{') < before.rfind('['): - continue # Slice syntax, no space required - if char == ',' and line[index + 1] == ')': - continue # Allow tuple with only one element: (3,) - yield 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: - yield 0, "E111 indentation is not a multiple of four" - indent_expect = previous_logical.endswith(':') - if indent_expect and indent_level <= previous_indent_level: - yield 0, "E112 expected an indented block" - if indent_level > previous_indent_level and not indent_expect: - yield 0, "E113 unexpected indentation" - - -def continuation_line_indentation(logical_line, tokens, indent_level, verbose): - r""" - Continuation lines should align wrapped elements either vertically using - Python's implicit line joining inside parentheses, brackets and braces, or - using a hanging indent. - - When using a hanging indent the following considerations should be applied: - - - there should be no arguments on the first line, and - - - further indentation should be used to clearly distinguish itself as a - continuation line. - - Okay: a = (\n) - E123: a = (\n ) - - Okay: a = (\n 42) - E121: a = (\n 42) - E122: a = (\n42) - E123: a = (\n 42\n ) - E124: a = (24,\n 42\n) - E125: if (a or\n b):\n pass - E126: a = (\n 42) - E127: a = (24,\n 42) - E128: a = (24,\n 42) - """ - first_row = tokens[0][2][0] - nrows = 1 + tokens[-1][2][0] - first_row - if nrows == 1: - return - - # indent_next tells us whether the next block is indented; assuming - # that it is indented by 4 spaces, then we should not allow 4-space - # indents on the final continuation line; in turn, some other - # indents are allowed to have an extra 4 spaces. - indent_next = logical_line.endswith(':') - - row = depth = 0 - # remember how many brackets were opened on each line - parens = [0] * nrows - # relative indents of physical lines - rel_indent = [0] * nrows - # visual indents - indent = [indent_level] - indent_chances = {} - last_indent = tokens[0][2] - if verbose >= 3: - print(">>> " + tokens[0][4].rstrip()) - - for token_type, text, start, end, line in tokens: - if line.strip().lower().endswith('# nopep8'): - continue - - newline = row < start[0] - first_row - if newline: - row = start[0] - first_row - newline = (not last_token_multiline and - token_type not in (tokenize.NL, tokenize.NEWLINE)) - - if newline: - # this is the beginning of a continuation line. - last_indent = start - if verbose >= 3: - print("... " + line.rstrip()) - - # record the initial indent. - rel_indent[row] = start[1] - indent_level - - if depth: - # a bracket expression in a continuation line. - # find the line that it was opened on - for open_row in range(row - 1, -1, -1): - if parens[open_row]: - break - else: - # an unbracketed continuation line (ie, backslash) - open_row = 0 - hang = rel_indent[row] - rel_indent[open_row] - visual_indent = indent_chances.get(start[1]) - - if token_type == tokenize.OP and text in ']})': - # this line starts with a closing bracket - if indent[depth]: - if start[1] != indent[depth]: - yield (start, 'E124 closing bracket does not match ' - 'visual indentation') - elif hang: - yield (start, 'E123 closing bracket does not match ' - 'indentation of opening bracket\'s line') - elif visual_indent is True: - # visual indent is verified - if not indent[depth]: - indent[depth] = start[1] - elif visual_indent in (text, str): - # ignore token lined up with matching one from a previous line - pass - elif indent[depth] and start[1] < indent[depth]: - # visual indent is broken - yield (start, 'E128 continuation line ' - 'under-indented for visual indent') - elif hang == 4 or (indent_next and rel_indent[row] == 8): - # hanging indent is verified - pass - else: - # indent is broken - if hang <= 0: - error = 'E122', 'missing indentation or outdented' - elif indent[depth]: - error = 'E127', 'over-indented for visual indent' - elif hang % 4: - error = 'E121', 'indentation is not a multiple of four' - else: - error = 'E126', 'over-indented for hanging indent' - yield start, "%s continuation line %s" % error - - # look for visual indenting - if (parens[row] and token_type not in (tokenize.NL, tokenize.COMMENT) - and not indent[depth]): - indent[depth] = start[1] - indent_chances[start[1]] = True - if verbose >= 4: - print("bracket depth %s indent to %s" % (depth, start[1])) - # deal with implicit string concatenation - elif (token_type in (tokenize.STRING, tokenize.COMMENT) or - text in ('u', 'ur', 'b', 'br')): - indent_chances[start[1]] = str - - # keep track of bracket depth - if token_type == tokenize.OP: - if text in '([{': - depth += 1 - indent.append(0) - parens[row] += 1 - if verbose >= 4: - print("bracket depth %s seen, col %s, visual min = %s" % - (depth, start[1], indent[depth])) - elif text in ')]}' and depth > 0: - # parent indents should not be more than this one - prev_indent = indent.pop() or last_indent[1] - for d in range(depth): - if indent[d] > prev_indent: - indent[d] = 0 - for ind in list(indent_chances): - if ind >= prev_indent: - del indent_chances[ind] - depth -= 1 - if depth: - indent_chances[indent[depth]] = True - for idx in range(row, -1, -1): - if parens[idx]: - parens[idx] -= 1 - break - assert len(indent) == depth + 1 - if start[1] not in indent_chances: - # allow to line up tokens - indent_chances[start[1]] = text - - last_token_multiline = (start[0] != end[0]) - - if indent_next and rel_indent[-1] == 4: - yield (last_indent, "E125 continuation line does not distinguish " - "itself from next logical line") - - -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 or prev_text in '}])') and - # Syntax "class A (B):" is allowed, but avoid it - (index < 2 or tokens[index - 2][1] != 'class') and - # Allow "return (a.foo for a in range(5))" - not keyword.iskeyword(prev_text)): - yield prev_end, "E211 whitespace before '%s'" % text - prev_type = token_type - prev_text = text - prev_end = end - - -def whitespace_around_operator(logical_line): - r""" - 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 - """ - for match in OPERATOR_REGEX.finditer(logical_line): - before, after = match.groups() - - if '\t' in before: - yield match.start(1), "E223 tab before operator" - elif len(before) > 1: - yield match.start(1), "E221 multiple spaces before operator" - - if '\t' in after: - yield match.start(2), "E224 tab after operator" - elif len(after) > 1: - yield match.start(2), "E222 multiple spaces 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) - Okay: z = 2 ** 30 - Okay: x = x / 2 - 1 - - E225: i=i+1 - E225: submitted +=1 - E225: c = alpha -4 - E225: x = x /2 - 1 - E225: z = x **y - E226: c = (a+b) * (a-b) - E226: z = 2**30 - E226: x = x*2 - 1 - E226: x = x/2 - 1 - E226: hypot2 = x*x + y*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 3 - continue - if text in ('(', 'lambda'): - parens += 1 - elif text == ')': - parens -= 1 - if need_space: - if start != prev_end: - # Found a (probably) needed space - if need_space is not True and not need_space[1]: - yield (need_space[0], - "E225 missing whitespace around operator") - need_space = False - elif text == '>' and prev_text in ('<', '-'): - # Tolerate the "<>" operator, even if running Python 3 - # Deal with Python 3's annotated return value "->" - pass - else: - if need_space is True or need_space[1]: - # A needed trailing space was not found - yield prev_end, "E225 missing whitespace around operator" - else: - yield (need_space[0], - "E226 missing optional whitespace around operator") - need_space = False - elif token_type == tokenize.OP and prev_end is not None: - if text == '=' and parens: - # Allow keyword args or defaults: foo(bar=None). - pass - elif text in WS_NEEDED_OPERATORS: - need_space = True - elif text in UNARY_OPERATORS: - # Check if the operator is being used as a binary operator - # Allow unary operators: -123, -x, +1. - # Allow argument unpacking: foo(*args, **kwargs). - if prev_type == tokenize.OP: - binary_usage = (prev_text in '}])') - elif prev_type == tokenize.NAME: - binary_usage = (prev_text not in KEYWORDS) - else: - binary_usage = (prev_type not in SKIP_TOKENS) - - if binary_usage: - if text in WS_OPTIONAL_OPERATORS: - need_space = None - else: - need_space = True - elif text in WS_OPTIONAL_OPERATORS: - need_space = None - - if need_space is None: - # Surrounding space is optional, but ensure that - # trailing space matches opening space - need_space = (prev_end, start != prev_end) - elif need_space and start == prev_end: - # A needed opening space was not found - yield prev_end, "E225 missing whitespace around operator" - need_space = False - prev_type = token_type - prev_text = text - prev_end = end - - -def whitespace_around_comma(logical_line): - r""" - Avoid extraneous whitespace in the following situations: - - - More than one space around an assignment (or other) operator to - align it with another. - - Note: these checks are disabled by default - - Okay: a = (1, 2) - E241: a = (1, 2) - E242: a = (1,\t2) - """ - line = logical_line - for m in WHITESPACE_AFTER_COMMA_REGEX.finditer(line): - found = m.start() + 1 - if '\t' in m.group(): - yield found, "E242 tab after '%s'" % m.group()[0] - else: - yield found, "E241 multiple spaces after '%s'" % m.group()[0] - - -def whitespace_around_named_parameter_equals(logical_line, tokens): - """ - 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 - no_space = False - prev_end = None - for token_type, text, start, end, line in tokens: - if no_space: - no_space = False - if start != prev_end: - yield (prev_end, - "E251 no spaces around keyword / parameter equals") - elif token_type == tokenize.OP: - if text == '(': - parens += 1 - elif text == ')': - parens -= 1 - elif parens and text == '=': - no_space = True - if start != prev_end: - yield (prev_end, - "E251 no spaces around keyword / parameter equals") - prev_end = end - - -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.COMMENT: - if not line[:start[1]].strip(): - continue - if prev_end[0] == start[0] and start[1] < prev_end[1] + 2: - yield (prev_end, - "E261 at least two spaces before inline comment") - if text.startswith('# ') or not text.startswith('# '): - yield start, "E262 inline comment should start with '# '" - elif token_type != tokenize.NL: - 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 -1 < found and ';' not in line[:found]: - yield 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) - before.count('(') <= before.count(')') and # (Python 3 annotation) - not LAMBDA_REGEX.search(before)): # lambda x: x - yield found, "E701 multiple statements on one line (colon)" - found = line.find(';') - if -1 < found: - yield found, "E702 multiple statements on one line (semicolon)" - - -def explicit_line_join(logical_line, tokens): - r""" - Avoid explicit line join between brackets. - - The preferred way of wrapping long lines is by using Python's implied line - continuation inside parentheses, brackets and braces. Long lines can be - broken over multiple lines by wrapping expressions in parentheses. These - should be used in preference to using a backslash for line continuation. - - E502: aaa = [123, \\n 123] - E502: aaa = ("bbb " \\n "ccc") - - Okay: aaa = [123,\n 123] - Okay: aaa = ("bbb "\n "ccc") - Okay: aaa = "bbb " \\n "ccc" - """ - prev_start = prev_end = parens = 0 - for token_type, text, start, end, line in tokens: - if start[0] != prev_start and parens and backslash: - yield backslash, "E502 the backslash is redundant between brackets" - if end[0] != prev_end: - if line.rstrip('\r\n').endswith('\\'): - backslash = (end[0], len(line.splitlines()[-1]) - 1) - else: - backslash = None - prev_start = prev_end = end[0] - else: - prev_start = start[0] - if token_type == tokenize.OP: - if text in '([{': - parens += 1 - elif text in ')]}': - parens -= 1 - - -def comparison_to_singleton(logical_line): - """ - Comparisons to singletons like None should always be done - with "is" or "is not", never the equality operators. - - Okay: if arg is not None: - E711: if arg != None: - E712: if arg == True: - - Also, beware of writing if x when you really mean if x is not None -- - e.g. when testing whether a variable or argument that defaults to None was - set to some other value. The other value might have a type (such as a - container) that could be false in a boolean context! - """ - match = COMPARE_SINGLETON_REGEX.search(logical_line) - if match: - same = (match.group(1) == '==') - singleton = match.group(2) - msg = "'if cond is %s:'" % (('' if same else 'not ') + singleton) - if singleton in ('None',): - code = 'E711' - else: - code = 'E712' - nonzero = ((singleton == 'True' and same) or - (singleton == 'False' and not same)) - msg += " or 'if %scond:'" % ('' if nonzero else 'not ') - yield match.start(1), ("%s comparison to %s should be %s" % - (code, singleton, msg)) - - -def comparison_type(logical_line): - """ - Object type comparisons should always use isinstance() instead of - comparing types directly. - - Okay: if isinstance(obj, int): - E721: if type(obj) is type(1): - - When checking if an object is a string, keep in mind that it might be a - unicode string too! In Python 2.3, str and unicode have a common base - class, basestring, so you can do: - - Okay: if isinstance(obj, basestring): - Okay: if type(a1) is type(b1): - """ - match = COMPARE_TYPE_REGEX.search(logical_line) - if match: - inst = match.group(3) - if inst and isidentifier(inst) and inst not in SINGLETONS: - return # Allow comparison for types which are not obvious - yield match.start(1), "E721 do not compare types, use 'isinstance()'" - - -def python_3000_has_key(logical_line): - r""" - The {}.has_key() method is removed in the Python 3. - Use the 'in' operation instead. - - Okay: if "alph" in d:\n print d["alph"] - W601: assert d.has_key('alph') - """ - pos = logical_line.find('.has_key(') - if pos > -1: - yield 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 is removed in Python 3. - - Okay: raise DummyError("Message") - W602: raise DummyError, "Message" - """ - match = RAISE_COMMA_REGEX.match(logical_line) - if match and ',' not in match.group(1): - yield match.start(1) - 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 3. - - Okay: if a != 'no': - W603: if a <> 'no': - """ - pos = logical_line.find('<>') - if pos > -1: - yield pos, "W603 '<>' is deprecated, use '!='" - - -def python_3000_backticks(logical_line): - """ - Backticks are removed in Python 3. - Use repr() instead. - - Okay: val = repr(1 + 2) - W604: val = `1 + 2` - """ - pos = logical_line.find('`') - if pos > -1: - yield pos, "W604 backticks are deprecated, use 'repr()'" - - -############################################################################## -# Helper functions -############################################################################## - - -if '' == ''.encode(): - # Python 2: implicit encoding. - def readlines(filename): - f = open(filename) - try: - return f.readlines() - finally: - f.close() - - isidentifier = re.compile(r'[a-zA-Z_]\w*').match - stdin_get_value = sys.stdin.read -else: - # Python 3 - def readlines(filename): - f = open(filename, 'rb') - try: - coding, lines = tokenize.detect_encoding(f.readline) - f = TextIOWrapper(f, coding, line_buffering=True) - return [l.decode(coding) for l in lines] + f.readlines() - except (LookupError, SyntaxError, UnicodeError): - f.close() - # Fall back if files are improperly declared - f = open(filename, encoding='latin-1') - return f.readlines() - finally: - f.close() - - isidentifier = str.isidentifier - - def stdin_get_value(): - return TextIOWrapper(sys.stdin.buffer, errors='ignore').read() -readlines.__doc__ = " Read the source code." - - -def expand_indent(line): - r""" - 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 - """ - if '\t' not in line: - return len(line) - len(line.lstrip()) - 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'" - """ - # String modifiers (e.g. u or r) - start = text.index(text[-1]) + 1 - end = len(text) - 1 - # Triple quotes - if text[-3:] in ('"""', "'''"): - start += 2 - end -= 2 - return text[:start] + 'x' * (end - start) + text[end:] - - -def parse_udiff(diff, patterns=None, parent='.'): - rv = {} - path = nrows = None - for line in diff.splitlines(): - if nrows: - if line[:1] != '-': - nrows -= 1 - continue - if line[:3] == '@@ ': - hunk_match = HUNK_REGEX.match(line) - row, nrows = [int(g or '1') for g in hunk_match.groups()] - rv[path].update(range(row, row + nrows)) - elif line[:3] == '+++': - path = line[4:].split('\t', 1)[0] - if path[:2] == 'b/': - path = path[2:] - rv[path] = set() - return dict([(os.path.join(parent, path), rows) - for (path, rows) in rv.items() - if rows and filename_match(path, patterns)]) - - -def filename_match(filename, patterns, default=True): - """ - Check if patterns contains a pattern that matches filename. - If patterns is unspecified, this always returns True. - """ - if not patterns: - return default - return any(fnmatch(filename, pattern) for pattern in patterns) - - -############################################################################## -# Framework to run all checks -############################################################################## - - -def find_checks(argument_name): - """ - Find all globally visible functions where the first argument name - starts with argument_name. - """ - 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(function.__doc__ or '') - yield name, codes, function, args - - -class Checker(object): - """ - Load a Python source file, tokenize it, check coding style. - """ - - def __init__(self, filename=None, lines=None, - options=None, report=None, **kwargs): - if options is None: - options = StyleGuide(kwargs).options - else: - assert not kwargs - self._io_error = None - self._physical_checks = options.physical_checks - self._logical_checks = options.logical_checks - self.max_line_length = options.max_line_length - self.verbose = options.verbose - self.filename = filename - if filename is None: - self.filename = 'stdin' - self.lines = lines or [] - elif filename == '-': - self.filename = 'stdin' - self.lines = stdin_get_value().splitlines(True) - elif lines is None: - try: - self.lines = readlines(filename) - except IOError: - exc_type, exc = sys.exc_info()[:2] - self._io_error = '%s: %s' % (exc_type.__name__, exc) - self.lines = [] - else: - self.lines = lines - self.report = report or options.report - self.report_error = self.report.error - - 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 line[:1] in WHITESPACE: - self.indent_char = line[0] - for name, check, argument_names in self._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 SKIP_TOKENS: - continue - if token_type == tokenize.STRING: - text = mute_string(text) - if previous: - end_row, end = previous[3] - start_row, start = token[2] - if end_row != start_row: # different row - prev_text = self.lines[end_row - 1][end - 1] - if prev_text == ',' or (prev_text not in '{[(' - and text not in '}])'): - logical.append(' ') - length += 1 - elif end != start: # different column - fill = self.lines[end_row - 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.strip() == self.logical_line - - def check_logical(self): - """ - Build a line from tokens and run all logical checks on it. - """ - self.build_tokens_line() - self.report.increment_logical_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 self.verbose >= 2: - print(self.logical_line[:80].rstrip()) - for name, check, argument_names in self._logical_checks: - if self.verbose >= 4: - print(' ' + name) - for result in self.run_check(check, argument_names): - offset, text = result - if isinstance(offset, tuple): - orig_number, orig_offset = offset - else: - for token_offset, token in self.mapping: - if offset >= token_offset: - orig_number = token[2][0] - orig_offset = (token[2][1] + offset - token_offset) - self.report_error(orig_number, orig_offset, text, check) - self.previous_logical = self.logical_line - - def generate_tokens(self): - if self._io_error: - self.report_error(1, 0, 'E902 %s' % self._io_error, readlines) - tokengen = tokenize.generate_tokens(self.readline_check_physical) - try: - for token in tokengen: - yield token - except (SyntaxError, tokenize.TokenError): - exc_type, exc = sys.exc_info()[:2] - offset = exc.args[1] - if len(offset) > 2: - offset = offset[1:3] - self.report_error(offset[0], offset[1], - 'E901 %s: %s' % (exc_type.__name__, exc.args[0]), - self.generate_tokens) - generate_tokens.__doc__ = " Check if the syntax is valid." - - def check_all(self, expected=None, line_offset=0): - """ - Run all checks on the input file. - """ - self.report.init_file(self.filename, self.lines, expected, line_offset) - self.line_number = 0 - self.indent_char = None - self.indent_level = 0 - self.previous_logical = '' - self.tokens = [] - self.blank_lines = blank_lines_before_comment = 0 - parens = 0 - for token in self.generate_tokens(): - self.tokens.append(token) - token_type, text = token[0:2] - if self.verbose >= 3: - if token[2][0] == token[3][0]: - pos = '[%s:%s]' % (token[2][1] or '', token[3][1]) - else: - pos = 'l.%s' % token[3][0] - print('l.%s\t%s\t%s\t%r' % - (token[2][0], pos, tokenize.tok_name[token[0]], text)) - if token_type == tokenize.OP: - if text in '([{': - parens += 1 - elif text in '}])': - parens -= 1 - elif not parens: - if token_type == tokenize.NEWLINE: - if self.blank_lines < blank_lines_before_comment: - self.blank_lines = blank_lines_before_comment - self.check_logical() - self.tokens = [] - self.blank_lines = blank_lines_before_comment = 0 - elif token_type == tokenize.NL: - if len(self.tokens) == 1: - # The physical line contains only this token. - self.blank_lines += 1 - self.tokens = [] - elif token_type == tokenize.COMMENT and len(self.tokens) == 1: - if blank_lines_before_comment < self.blank_lines: - blank_lines_before_comment = self.blank_lines - self.blank_lines = 0 - if COMMENT_WITH_NL: - # The comment also ends a physical line - self.tokens = [] - return self.report.get_file_results() - - -class BaseReport(object): - """Collect the results of the checks.""" - print_filename = False - - def __init__(self, options): - self._benchmark_keys = options.benchmark_keys - self._ignore_code = options.ignore_code - # Results - self.elapsed = 0 - self.total_errors = 0 - self.counters = dict.fromkeys(self._benchmark_keys, 0) - self.messages = {} - - def start(self): - """Start the timer.""" - self._start_time = time.time() - - def stop(self): - """Stop the timer.""" - self.elapsed = time.time() - self._start_time - - def init_file(self, filename, lines, expected, line_offset): - """Signal a new file.""" - self.filename = filename - self.lines = lines - self.expected = expected or () - self.line_offset = line_offset - self.file_errors = 0 - self.counters['files'] += 1 - self.counters['physical lines'] += len(lines) - - def increment_logical_line(self): - """Signal a new logical line.""" - self.counters['logical lines'] += 1 - - def error(self, line_number, offset, text, check): - """Report an error, according to options.""" - code = text[:4] - if self._ignore_code(code): - return - if code in self.counters: - self.counters[code] += 1 - else: - self.counters[code] = 1 - self.messages[code] = text[5:] - # Don't care about expected errors or warnings - if code in self.expected: - return - if self.print_filename and not self.file_errors: - print(self.filename) - self.file_errors += 1 - self.total_errors += 1 - return code - - def get_file_results(self): - """Return the count of errors and warnings for this file.""" - return self.file_errors - - def get_count(self, prefix=''): - """Return the total count of errors and warnings.""" - return sum([self.counters[key] - for key in self.messages if key.startswith(prefix)]) - - def get_statistics(self, 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 - """ - return ['%-7s %s %s' % (self.counters[key], key, self.messages[key]) - for key in sorted(self.messages) if key.startswith(prefix)] - - def print_statistics(self, prefix=''): - """Print overall statistics (number of errors and warnings).""" - for line in self.get_statistics(prefix): - print(line) - - def print_benchmark(self): - """Print benchmark numbers.""" - print('%-7.2f %s' % (self.elapsed, 'seconds elapsed')) - if self.elapsed: - for key in self._benchmark_keys: - print('%-7d %s per second (%d total)' % - (self.counters[key] / self.elapsed, key, - self.counters[key])) - - -class FileReport(BaseReport): - print_filename = True - - -class StandardReport(BaseReport): - """Collect and print the results of the checks.""" - - def __init__(self, options): - super(StandardReport, self).__init__(options) - self._fmt = REPORT_FORMAT.get(options.format.lower(), - options.format) - self._repeat = options.repeat - self._show_source = options.show_source - self._show_pep8 = options.show_pep8 - - def error(self, line_number, offset, text, check): - """ - Report an error, according to options. - """ - code = super(StandardReport, self).error(line_number, offset, - text, check) - if code and (self.counters[code] == 1 or self._repeat): - print(self._fmt % { - 'path': self.filename, - 'row': self.line_offset + line_number, 'col': offset + 1, - 'code': code, 'text': text[5:], - }) - if self._show_source: - if line_number > len(self.lines): - line = '' - else: - line = self.lines[line_number - 1] - print(line.rstrip()) - print(' ' * offset + '^') - if self._show_pep8: - print(check.__doc__.lstrip('\n').rstrip()) - return code - - -class DiffReport(StandardReport): - """Collect and print the results for the changed lines only.""" - - def __init__(self, options): - super(DiffReport, self).__init__(options) - self._selected = options.selected_lines - - def error(self, line_number, offset, text, check): - if line_number not in self._selected[self.filename]: - return - return super(DiffReport, self).error(line_number, offset, text, check) - - -class TestReport(StandardReport): - """Collect the results for the tests.""" - - def __init__(self, options): - options.benchmark_keys += ['test cases', 'failed tests'] - super(TestReport, self).__init__(options) - self._verbose = options.verbose - - def get_file_results(self): - # Check if the expected errors were found - label = '%s:%s:1' % (self.filename, self.line_offset) - codes = sorted(self.expected) - for code in codes: - if not self.counters.get(code): - self.file_errors += 1 - self.total_errors += 1 - print('%s: error %s not found' % (label, code)) - if self._verbose and not self.file_errors: - print('%s: passed (%s)' % - (label, ' '.join(codes) or 'Okay')) - self.counters['test cases'] += 1 - if self.file_errors: - self.counters['failed tests'] += 1 - # Reset counters - for key in set(self.counters) - set(self._benchmark_keys): - del self.counters[key] - self.messages = {} - return self.file_errors - - def print_results(self): - results = ("%(physical lines)d lines tested: %(files)d files, " - "%(test cases)d test cases%%s." % self.counters) - if self.total_errors: - print(results % ", %s failures" % self.total_errors) - else: - print(results % "") - print("Test failed." if self.total_errors else "Test passed.") - - -class StyleGuide(object): - """Initialize a PEP-8 instance with few options.""" - - def __init__(self, *args, **kwargs): - # build options from the command line - parse_argv = kwargs.pop('parse_argv', False) - config_file = kwargs.pop('config_file', None) - options, self.paths = process_options(parse_argv=parse_argv, - config_file=config_file) - if args or kwargs: - # build options from dict - options_dict = dict(*args, **kwargs) - options.__dict__.update(options_dict) - if 'paths' in options_dict: - self.paths = options_dict['paths'] - - self.runner = self.input_file - self.options = options - - if not options.reporter: - options.reporter = BaseReport if options.quiet else StandardReport - - for index, value in enumerate(options.exclude): - options.exclude[index] = value.rstrip('/') - # Ignore all checks which are not explicitly selected - options.select = tuple(options.select or ()) - options.ignore = tuple(options.ignore or options.select and ('',)) - options.benchmark_keys = BENCHMARK_KEYS[:] - options.ignore_code = self.ignore_code - options.physical_checks = self.get_checks('physical_line') - options.logical_checks = self.get_checks('logical_line') - self.init_report() - - def init_report(self, reporter=None): - """Initialize the report instance.""" - self.options.report = (reporter or self.options.reporter)(self.options) - return self.options.report - - def check_files(self, paths=None): - """Run all checks on the paths.""" - if paths is None: - paths = self.paths - report = self.options.report - runner = self.runner - report.start() - for path in paths: - if os.path.isdir(path): - self.input_dir(path) - elif not self.excluded(path): - runner(path) - report.stop() - return report - - def input_file(self, filename, lines=None, expected=None, line_offset=0): - """Run all checks on a Python source file.""" - if self.options.verbose: - print('checking %s' % filename) - fchecker = Checker(filename, lines=lines, options=self.options) - return fchecker.check_all(expected=expected, line_offset=line_offset) - - def input_dir(self, dirname): - """Check all files in this directory and all subdirectories.""" - dirname = dirname.rstrip('/') - if self.excluded(dirname): - return 0 - counters = self.options.report.counters - verbose = self.options.verbose - filepatterns = self.options.filename - runner = self.runner - for root, dirs, files in os.walk(dirname): - if verbose: - print('directory ' + root) - counters['directories'] += 1 - for subdir in sorted(dirs): - if self.excluded(os.path.join(root, subdir)): - dirs.remove(subdir) - for filename in sorted(files): - # contain a pattern that matches? - if ((filename_match(filename, filepatterns) and - not self.excluded(filename))): - runner(os.path.join(root, filename)) - - def excluded(self, filename): - """ - Check if options.exclude contains a pattern that matches filename. - """ - basename = os.path.basename(filename) - return any((filename_match(filename, self.options.exclude, - default=False), - filename_match(basename, self.options.exclude, - default=False))) - - def ignore_code(self, code): - """ - Check if the error code should be ignored. - - If 'options.select' contains a prefix of the error code, - return False. Else, if 'options.ignore' contains a prefix of - the error code, return True. - """ - return (code.startswith(self.options.ignore) and - not code.startswith(self.options.select)) - - def get_checks(self, argument_name): - """ - Find all globally visible functions where the first argument name - starts with argument_name and which contain selected tests. - """ - checks = [] - for name, codes, function, args in find_checks(argument_name): - if any(not (code and self.ignore_code(code)) for code in codes): - checks.append((name, function, args)) - return sorted(checks) - - -def init_tests(pep8style): - """ - Initialize testing framework. - - A test file can provide many tests. Each test starts with a - declaration. This declaration is a single line starting with '#:'. - It declares codes of expected failures, separated by spaces or 'Okay' - if no failure is expected. - If the file does not contain such declaration, it should pass all - tests. If the declaration is empty, following lines are not checked, - until next declaration. - - Examples: - - * Only E224 and W701 are expected: #: E224 W701 - * Following example is conform: #: Okay - * Don't check these lines: #: - """ - report = pep8style.init_report(TestReport) - runner = pep8style.input_file - - def run_tests(filename): - """Run all the tests from a file.""" - lines = readlines(filename) + ['#:\n'] - line_offset = 0 - codes = ['Okay'] - testcase = [] - count_files = report.counters['files'] - for index, line in enumerate(lines): - if not line.startswith('#:'): - if codes: - # Collect the lines of the test case - testcase.append(line) - continue - if codes and index: - codes = [c for c in codes if c != 'Okay'] - # Run the checker - runner(filename, testcase, expected=codes, - line_offset=line_offset) - # output the real line numbers - line_offset = index + 1 - # configure the expected errors - codes = line.split()[1:] - # empty the test case buffer - del testcase[:] - report.counters['files'] = count_files + 1 - return report.counters['failed tests'] - - pep8style.runner = run_tests - - -def selftest(options): - """ - Test all check functions with test cases in docstrings. - """ - count_failed = count_all = 0 - report = BaseReport(options) - counters = report.counters - 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() - lines = [part.replace(r'\t', '\t') + '\n' - for part in source.split(r'\n')] - checker = Checker(lines=lines, options=options, report=report) - checker.check_all() - error = None - if code == 'Okay': - if len(counters) > len(options.benchmark_keys): - codes = [key for key in counters - if key not in options.benchmark_keys] - error = "incorrectly found %s" % ', '.join(codes) - elif not counters.get(code): - error = "failed to find %s" % code - # Keep showing errors for multiple tests - for key in set(counters) - set(options.benchmark_keys): - del counters[key] - report.messages = {} - count_all += 1 - if not error: - if options.verbose: - print("%s: %s" % (code, source)) - else: - count_failed += 1 - print("%s: %s:" % (__file__, error)) - for line in checker.lines: - print(line.rstrip()) - return count_failed, count_all - - -def read_config(options, args, arglist, parser): - """Read both user configuration and local configuration.""" - config = RawConfigParser() - - user_conf = options.config - if user_conf and os.path.isfile(user_conf): - if options.verbose: - print('user configuration: %s' % user_conf) - config.read(user_conf) - - parent = tail = args and os.path.abspath(os.path.commonprefix(args)) - while tail: - local_conf = os.path.join(parent, '.pep8') - if os.path.isfile(local_conf): - if options.verbose: - print('local configuration: %s' % local_conf) - config.read(local_conf) - break - parent, tail = os.path.split(parent) - - if config.has_section('pep8'): - option_list = dict([(o.dest, o.type or o.action) - for o in parser.option_list]) - - # First, read the default values - new_options, _ = parser.parse_args([]) - - # Second, parse the configuration - for opt in config.options('pep8'): - if options.verbose > 1: - print(' %s = %s' % (opt, config.get('pep8', opt))) - if opt.replace('_', '-') not in parser.config_options: - print('Unknown option: \'%s\'\n not in [%s]' % - (opt, ' '.join(parser.config_options))) - sys.exit(1) - normalized_opt = opt.replace('-', '_') - opt_type = option_list[normalized_opt] - if opt_type in ('int', 'count'): - value = config.getint('pep8', opt) - elif opt_type == 'string': - value = config.get('pep8', opt) - else: - assert opt_type in ('store_true', 'store_false') - value = config.getboolean('pep8', opt) - setattr(new_options, normalized_opt, value) - - # Third, overwrite with the command-line options - options, _ = parser.parse_args(arglist, values=new_options) - - return options - - -def process_options(arglist=None, parse_argv=False, config_file=None): - """Process options passed either via arglist or via command line args.""" - if not arglist and not parse_argv: - # Don't read the command line if the module is used as a library. - arglist = [] - if config_file is True: - config_file = DEFAULT_CONFIG - - parser = OptionParser(version=__version__, - usage="%prog [options] input ...") - parser.config_options = [ - 'builtins', 'count', 'exclude', 'filename', 'format', 'ignore', - 'max-line-length', 'quiet', 'select', 'show-pep8', 'show-source', - 'statistics', 'verbose', - ] - parser.add_option('--builtins', default='', - help="append builtin function (pyflakes " - "_MAGIC_GLOBALS)") - 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', default=True, action='store_true', - help="(obsolete) show all occurrences of the same error") - parser.add_option('--first', action='store_false', dest='repeat', - help="show first occurrence of each error") - parser.add_option('--exclude', metavar='patterns', default=DEFAULT_EXCLUDE, - help="exclude files or directories which match these " - "comma separated patterns (default: %default)") - parser.add_option('--filename', metavar='patterns', default='*.py', - help="when parsing directories, only check filenames " - "matching these comma separated patterns " - "(default: %default)") - 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 " - "(implies --first)") - 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('--max-line-length', type='int', metavar='n', - default=MAX_LINE_LENGTH, - help="set maximum allowed line length " - "(default: %default)") - parser.add_option('--format', metavar='format', default='default', - help="set the error format [default|pylint|]") - parser.add_option('--diff', action='store_true', - help="report only lines changed according to the " - "unified diff received on STDIN") - group = parser.add_option_group("Testing Options") - group.add_option('--testsuite', metavar='dir', - help="run regression tests from dir") - group.add_option('--doctest', action='store_true', - help="run doctest on myself") - group.add_option('--benchmark', action='store_true', - help="measure processing speed") - group = parser.add_option_group("Configuration", description=( - "The project options are read from the [pep8] section of the .pep8 " - "file located in any parent folder of the path(s) being processed. " - "Allowed options are: %s." % ', '.join(parser.config_options))) - group.add_option('--config', metavar='path', default=config_file, - help="config file location (default: %default)") - parser.add_option('--exit-zero', action='store_true', - help="use exit code 0 (success), even if there are " - "warnings") - parser.add_option('--max-complexity', default=-1, action='store', - type='int', help="McCabe complexity threshold") - - options, args = parser.parse_args(arglist) - options.reporter = None - - if options.testsuite: - args.append(options.testsuite) - elif not options.doctest: - if parse_argv and not args: - if os.path.exists('.pep8') or options.diff: - args = ['.'] - else: - parser.error('input not specified') - options = read_config(options, args, arglist, parser) - options.reporter = parse_argv and options.quiet == 1 and FileReport - - if options.builtins: - options.builtins = options.builtins.split(',') - if options.filename: - options.filename = options.filename.split(',') - options.exclude = options.exclude.split(',') - if options.select: - options.select = options.select.split(',') - if options.ignore: - options.ignore = options.ignore.split(',') - elif not (options.select or - options.testsuite or options.doctest) and DEFAULT_IGNORE: - # The default choice: ignore controversial checks - # (for doctest and testsuite, all checks are required) - options.ignore = DEFAULT_IGNORE.split(',') - - if options.diff: - options.reporter = DiffReport - stdin = stdin_get_value() - options.selected_lines = parse_udiff(stdin, options.filename, args[0]) - args = sorted(options.selected_lines) - - return options, args - - -def _main(): - """Parse options and run checks on Python source.""" - pep8style = StyleGuide(parse_argv=True, config_file=True) - options = pep8style.options - if options.doctest: - import doctest - fail_d, done_d = doctest.testmod(report=False, verbose=options.verbose) - fail_s, done_s = selftest(options) - count_failed = fail_s + fail_d - if not options.quiet: - count_passed = done_d + done_s - count_failed - print("%d passed and %d failed." % (count_passed, count_failed)) - print("Test failed." if count_failed else "Test passed.") - if count_failed: - sys.exit(1) - if options.testsuite: - init_tests(pep8style) - report = pep8style.check_files() - if options.statistics: - report.print_statistics() - if options.benchmark: - report.print_benchmark() - if options.testsuite and not options.quiet: - report.print_results() - if report.total_errors: - if options.count: - sys.stderr.write(str(report.total_errors) + '\n') - sys.exit(1) - -if __name__ == '__main__': - _main() From c9a5923a46bf8200d700424896c52a496516327d Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 29 Dec 2012 22:14:43 -0500 Subject: [PATCH 05/20] I was wrong. Forgot a couple lines --- flake8/run.py | 47 ++--------------------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/flake8/run.py b/flake8/run.py index 9a6cf2f..6353dc5 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -104,7 +104,7 @@ def main(): pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) options = pep8style.options - complexity = options.max_complexity + complexity = opts.max_complexity builtins = set(opts.builtins.split(',')) warnings = 0 stdin = None @@ -125,7 +125,7 @@ def main(): stdin = read_stdin() warnings += check_code(stdin, opts.ignore, complexity) - if options.exit_zero: + if opts.exit_zero: raise SystemExit(0) raise SystemExit(warnings) @@ -186,49 +186,6 @@ def run(command): [line.strip() for line in p.stderr.readlines()]) -def git_hook(complexity=-1, strict=False, ignore=None, lazy=False): - _initpep8() - if ignore: - pep8style.options.ignore = ignore - - warnings = 0 - - gitcmd = "git diff-index --cached --name-only HEAD" - if lazy: - gitcmd = gitcmd.replace('--cached ', '') - - _, files_modified, _ = run(gitcmd) - for filename in files_modified: - ext = os.path.splitext(filename)[-1] - if ext != '.py': - continue - if not os.path.exists(filename): - continue - warnings += check_file(path=filename, ignore=ignore, - complexity=complexity) - - if strict: - return warnings - - return 0 - - -def hg_hook(ui, repo, **kwargs): - _initpep8() - complexity = ui.config('flake8', 'complexity', default=-1) - warnings = 0 - - for file_ in _get_files(repo, **kwargs): - warnings += check_file(file_, complexity) - - strict = ui.configbool('flake8', 'strict', default=True) - - if strict: - return warnings - - return 0 - - try: from setuptools import Command except ImportError: From 8810532989eb6cd6944f1a7e88269066acf256f4 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 29 Dec 2012 22:15:33 -0500 Subject: [PATCH 06/20] flake8/ now passes flake8 --- flake8/mccabe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake8/mccabe.py b/flake8/mccabe.py index d998d0e..e93c87f 100644 --- a/flake8/mccabe.py +++ b/flake8/mccabe.py @@ -58,8 +58,8 @@ class PathNode: self.look = look def to_dot(self): - print('node [shape=%s,label="%s"] %d;' % - (self.look, self.name, self.dot_id())) + print('node [shape=%s,label="%s"] %d;' % ( + self.look, self.name, self.dot_id())) def dot_id(self): return id(self) From 7200f8f4bd13f88da7705af5742945ac40049c12 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sun, 30 Dec 2012 18:05:30 -0500 Subject: [PATCH 07/20] Filter out our options before passing to pep8 Update the .hgignore with a few pet peeves. --- .hgignore | 2 ++ flake8/run.py | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.hgignore b/.hgignore index 239c799..41cb134 100644 --- a/.hgignore +++ b/.hgignore @@ -6,3 +6,5 @@ bin flake8.egg-info man \.Python +nose* +.*\.swp diff --git a/flake8/run.py b/flake8/run.py index 6353dc5..fadf5d7 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -75,7 +75,8 @@ def get_parser(): sys.exit(0) # Create our own parser - parser = optparse.OptionParser('%prog [options]', version=version) + parser = optparse.OptionParser('%prog [options] [file.py|directory]', + version=version) parser.version = '{0} (pep8: {1}, flakey: {2})'.format( __version__, pep8.__version__, flakey.__version__) parser.remove_option('--version') @@ -98,9 +99,12 @@ def get_parser(): def main(): global pep8style + # parse out our flags so pep8 doesn't get confused parser = get_parser() - # parse our flags - opts, args = parser.parse_args() + opts, sys.argv = parser.parse_args() + + # make sure pep8 gets the information it expects + sys.argv.insert(0, 'pep8') pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) options = pep8style.options From 08099588df0cbd811678b6a87cd7976af74cf5a3 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Tue, 1 Jan 2013 22:48:56 -0500 Subject: [PATCH 08/20] This is what I meant to do in a03347e --- flake8/run.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/flake8/run.py b/flake8/run.py index fadf5d7..858aaa3 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -190,6 +190,49 @@ def run(command): [line.strip() for line in p.stderr.readlines()]) +def git_hook(complexity=-1, strict=False, ignore=None, lazy=False): + _initpep8() + if ignore: + pep8style.options.ignore = ignore + + warnings = 0 + + gitcmd = "git diff-index --cached --name-only HEAD" + if lazy: + gitcmd = gitcmd.replace('--cached ', '') + + _, files_modified, _ = run(gitcmd) + for filename in files_modified: + ext = os.path.splitext(filename)[-1] + if ext != '.py': + continue + if not os.path.exists(filename): + continue + warnings += check_file(path=filename, ignore=ignore, + complexity=complexity) + + if strict: + return warnings + + return 0 + + +def hg_hook(ui, repo, **kwargs): + _initpep8() + complexity = ui.config('flake8', 'complexity', default=-1) + warnings = 0 + + for file_ in _get_files(repo, **kwargs): + warnings += check_file(file_, complexity) + + strict = ui.configbool('flake8', 'strict', default=True) + + if strict: + return warnings + + return 0 + + try: from setuptools import Command except ImportError: From 3bd8a21f62db68074289960b1a2ca5bc0b0e2308 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Tue, 1 Jan 2013 23:09:33 -0500 Subject: [PATCH 09/20] Split up flake8/run.py I've preserved the API for users expecting their hooks and other things to work. That said, I'm working on #51 now as well. --- flake8/hooks.py | 80 ++++++++++++++ flake8/main.py | 169 +++++++++++++++++++++++++++++ flake8/run.py | 274 +----------------------------------------------- flake8/util.py | 17 +++ 4 files changed, 270 insertions(+), 270 deletions(-) create mode 100644 flake8/hooks.py create mode 100644 flake8/main.py diff --git a/flake8/hooks.py b/flake8/hooks.py new file mode 100644 index 0000000..970fffa --- /dev/null +++ b/flake8/hooks.py @@ -0,0 +1,80 @@ +import os +from flake8.util import _initpep8, pep8style, skip_file +from subprocess import Popen, PIPE + + +def git_hook(complexity=-1, strict=False, ignore=None, lazy=False): + from flake8.main import check_file + _initpep8() + if ignore: + pep8style.options.ignore = ignore + + warnings = 0 + + gitcmd = "git diff-index --cached --name-only HEAD" + if lazy: + gitcmd = gitcmd.replace('--cached ', '') + + _, files_modified, _ = run(gitcmd) + for filename in files_modified: + ext = os.path.splitext(filename)[-1] + if ext != '.py': + continue + if not os.path.exists(filename): + continue + warnings += check_file(path=filename, ignore=ignore, + complexity=complexity) + + if strict: + return warnings + + return 0 + + +def hg_hook(ui, repo, **kwargs): + from flake8.main import check_file + _initpep8() + complexity = ui.config('flake8', 'complexity', default=-1) + warnings = 0 + + for file_ in _get_files(repo, **kwargs): + warnings += check_file(file_, complexity) + + strict = ui.configbool('flake8', 'strict', default=True) + + if strict: + return warnings + + return 0 + + +def run(command): + p = Popen(command.split(), stdout=PIPE, stderr=PIPE) + p.wait() + return (p.returncode, [line.strip() for line in p.stdout.readlines()], + [line.strip() for line in p.stderr.readlines()]) + + +def _get_files(repo, **kwargs): + seen = set() + for rev in range(repo[kwargs['node']], len(repo)): + for file_ in repo[rev].files(): + file_ = os.path.join(repo.root, file_) + if file_ in seen or not os.path.exists(file_): + continue + seen.add(file_) + if not file_.endswith('.py'): + continue + if skip_file(file_): + continue + yield file_ + + +def find_vcs(): + if os.path.isdir('.git'): + if not os.path.isdir('.git/hooks'): + os.mkdir('.git/hooks') + return '.git/hooks/pre-commit' + elif os.path.isdir('.hg'): + return '.hg/hgrc' + return '' diff --git a/flake8/main.py b/flake8/main.py new file mode 100644 index 0000000..df92450 --- /dev/null +++ b/flake8/main.py @@ -0,0 +1,169 @@ +import os +import sys +import pep8 +import flakey +import select +import optparse +from flake8 import mccabe +from flake8 import __version__ +from flake8.util import _initpep8, skip_file + +pep8style = None + + +def get_parser(): + """Create a custom OptionParser""" + + def version(option, opt, value, parser): + parser.print_usage() + parser.print_version() + sys.exit(0) + + # Create our own parser + parser = optparse.OptionParser('%prog [options] [file.py|directory]', + version=version) + parser.version = '{0} (pep8: {1}, flakey: {2})'.format( + __version__, pep8.__version__, flakey.__version__) + parser.remove_option('--version') + # don't overlap with pep8's verbose option + parser.add_option('--builtins', default='', dest='builtins', + help="append builtin functions to flakey's " + "_MAGIC_BUILTINS") + parser.add_option('--ignore', default='', + help='skip errors and warnings (e.g. E4,W)') + parser.add_option('--exit-zero', action='store_true', default=False, + help='Exit with status 0 even if there are errors') + parser.add_option('--max-complexity', default=-1, action='store', + type='int', help='McCabe complexity threshold') + parser.add_option('-V', '--version', action='callback', + callback=version, + help='Print the version info for flake8') + return parser + + +def main(): + global pep8style + + # parse out our flags so pep8 doesn't get confused + parser = get_parser() + opts, sys.argv = parser.parse_args() + + # make sure pep8 gets the information it expects + sys.argv.insert(0, 'pep8') + + pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) + options = pep8style.options + complexity = opts.max_complexity + builtins = set(opts.builtins.split(',')) + warnings = 0 + stdin = None + + if builtins: + orig_builtins = set(flakey.checker._MAGIC_GLOBALS) + flakey.checker._MAGIC_GLOBALS = orig_builtins | builtins + + if pep8style.paths and options.filename is not None: + for path in _get_python_files(pep8style.paths): + if path == '-': + if stdin is None: + stdin = read_stdin() + warnings += check_code(stdin, opts.ignore, complexity) + else: + warnings += check_file(path, opts.ignore, complexity) + else: + stdin = read_stdin() + warnings += check_code(stdin, opts.ignore, complexity) + + if opts.exit_zero: + raise SystemExit(0) + + raise SystemExit(warnings) + + +def check_file(path, ignore=(), complexity=-1): + if pep8style.excluded(path): + return 0 + warning = flakey.checkPath(path) + warnings = flakey.print_messages(warning, ignore=ignore) + warnings += pep8style.input_file(path) + if complexity > -1: + warnings += mccabe.get_module_complexity(path, complexity) + return warnings + + +def check_code(code, ignore=(), complexity=-1): + warning = flakey.check(code, 'stdin') + warnings = flakey.print_messages(warning, ignore=ignore) + warnings += pep8style.input_file(None, lines=code.split('\n')) + if complexity > -1: + warnings += mccabe.get_code_complexity(code, complexity) + return warnings + + +def read_stdin(): + # wait for 1 second on the stdin fd + reads, __, __ = select.select([sys.stdin], [], [], 1.) + if reads == []: + print('input not specified') + raise SystemExit(1) + + return sys.stdin.read() + + +try: + from setuptools import Command +except ImportError: + Flake8Command = None +else: + class Flake8Command(Command): + description = "Run flake8 on modules registered in setuptools" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def distribution_files(self): + if self.distribution.packages: + for package in self.distribution.packages: + yield package.replace(".", os.path.sep) + + if self.distribution.py_modules: + for filename in self.distribution.py_modules: + yield "%s.py" % filename + + def run(self): + _initpep8() + + # _get_python_files can produce the same file several + # times, if one of its paths is a parent of another. Keep + # a set of checked files to de-duplicate. + checked = set() + + warnings = 0 + for path in _get_python_files(self.distribution_files()): + if path not in checked: + warnings += check_file(path) + checked.add(path) + + raise SystemExit(warnings > 0) + + +def _get_python_files(paths): + for path in paths: + if os.path.isdir(path): + for dirpath, dirnames, filenames in os.walk(path): + if pep8style.excluded(dirpath): + continue + for filename in filenames: + if not filename.endswith('.py'): + continue + fullpath = os.path.join(dirpath, filename) + if not skip_file(fullpath) or pep8style.excluded(fullpath): + yield fullpath + + else: + if not skip_file(path) or pep8style.excluded(path): + yield path diff --git a/flake8/run.py b/flake8/run.py index 858aaa3..e574c39 100644 --- a/flake8/run.py +++ b/flake8/run.py @@ -2,276 +2,10 @@ """ Implementation of the command-line I{flake8} tool. """ -import sys -import os -import os.path -import optparse -from subprocess import PIPE, Popen -import select - -from flake8 import mccabe -from flake8.util import skip_file -from flake8 import __version__ -import pep8 -import flakey - -pep8style = None - - -def check_file(path, ignore=(), complexity=-1): - if pep8style.excluded(path): - return 0 - warning = flakey.checkPath(path) - warnings = flakey.print_messages(warning, ignore=ignore) - warnings += pep8style.input_file(path) - if complexity > -1: - warnings += mccabe.get_module_complexity(path, complexity) - return warnings - - -def check_code(code, ignore=(), complexity=-1): - warning = flakey.check(code, 'stdin') - warnings = flakey.print_messages(warning, ignore=ignore) - warnings += pep8style.input_file(None, lines=code.split('\n')) - if complexity > -1: - warnings += mccabe.get_code_complexity(code, complexity) - return warnings - - -def _get_python_files(paths): - for path in paths: - if os.path.isdir(path): - for dirpath, dirnames, filenames in os.walk(path): - if pep8style.excluded(dirpath): - continue - for filename in filenames: - if not filename.endswith('.py'): - continue - fullpath = os.path.join(dirpath, filename) - if not skip_file(fullpath) or pep8style.excluded(fullpath): - yield fullpath - - else: - if not skip_file(path) or pep8style.excluded(path): - yield path - - -def read_stdin(): - # wait for 1 second on the stdin fd - reads, __, __ = select.select([sys.stdin], [], [], 1.) - if reads == []: - print('input not specified') - raise SystemExit(1) - - return sys.stdin.read() - - -def get_parser(): - """Create a custom OptionParser""" - - def version(option, opt, value, parser): - parser.print_usage() - parser.print_version() - sys.exit(0) - - # Create our own parser - parser = optparse.OptionParser('%prog [options] [file.py|directory]', - version=version) - parser.version = '{0} (pep8: {1}, flakey: {2})'.format( - __version__, pep8.__version__, flakey.__version__) - parser.remove_option('--version') - # don't overlap with pep8's verbose option - parser.add_option('--builtins', default='', dest='builtins', - help="append builtin functions to flakey's " - "_MAGIC_BUILTINS") - parser.add_option('--ignore', default='', - help='skip errors and warnings (e.g. E4,W)') - parser.add_option('--exit-zero', action='store_true', default=False, - help='Exit with status 0 even if there are errors') - parser.add_option('--max-complexity', default=-1, action='store', - type='int', help='McCabe complexity threshold') - parser.add_option('-V', '--version', action='callback', - callback=version, - help='Print the version info for flake8') - return parser - - -def main(): - global pep8style - - # parse out our flags so pep8 doesn't get confused - parser = get_parser() - opts, sys.argv = parser.parse_args() - - # make sure pep8 gets the information it expects - sys.argv.insert(0, 'pep8') - - pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) - options = pep8style.options - complexity = opts.max_complexity - builtins = set(opts.builtins.split(',')) - warnings = 0 - stdin = None - - if builtins: - orig_builtins = set(flakey.checker._MAGIC_GLOBALS) - flakey.checker._MAGIC_GLOBALS = orig_builtins | builtins - - if pep8style.paths and options.filename is not None: - for path in _get_python_files(pep8style.paths): - if path == '-': - if stdin is None: - stdin = read_stdin() - warnings += check_code(stdin, opts.ignore, complexity) - else: - warnings += check_file(path, opts.ignore, complexity) - else: - stdin = read_stdin() - warnings += check_code(stdin, opts.ignore, complexity) - - if opts.exit_zero: - raise SystemExit(0) - - raise SystemExit(warnings) - - -def _get_files(repo, **kwargs): - seen = set() - for rev in range(repo[kwargs['node']], len(repo)): - for file_ in repo[rev].files(): - file_ = os.path.join(repo.root, file_) - if file_ in seen or not os.path.exists(file_): - continue - seen.add(file_) - if not file_.endswith('.py'): - continue - if skip_file(file_): - continue - yield file_ - - -class _PEP8Options(object): - # Default options taken from pep8.process_options() - max_complexity = -1 - verbose = False - quiet = False - no_repeat = False - exclude = [exc.rstrip('/') for exc in pep8.DEFAULT_EXCLUDE.split(',')] - filename = ['*.py'] - select = [] - ignore = pep8.DEFAULT_IGNORE.split(',') # or []? - show_source = False - show_pep8 = False - statistics = False - count = False - benchmark = False - testsuite = '' - doctest = False - max_line_length = pep8.MAX_LINE_LENGTH - - -def _initpep8(): - # default pep8 setup - global pep8style - if pep8style is None: - pep8style = pep8.StyleGuide(config_file=True) - pep8style.options.physical_checks = pep8.find_checks('physical_line') - pep8style.options.logical_checks = pep8.find_checks('logical_line') - pep8style.options.counters = dict.fromkeys(pep8.BENCHMARK_KEYS, 0) - pep8style.options.messages = {} - pep8style.options.max_line_length = 79 - pep8style.args = [] - - -def run(command): - p = Popen(command.split(), stdout=PIPE, stderr=PIPE) - p.wait() - return (p.returncode, [line.strip() for line in p.stdout.readlines()], - [line.strip() for line in p.stderr.readlines()]) - - -def git_hook(complexity=-1, strict=False, ignore=None, lazy=False): - _initpep8() - if ignore: - pep8style.options.ignore = ignore - - warnings = 0 - - gitcmd = "git diff-index --cached --name-only HEAD" - if lazy: - gitcmd = gitcmd.replace('--cached ', '') - - _, files_modified, _ = run(gitcmd) - for filename in files_modified: - ext = os.path.splitext(filename)[-1] - if ext != '.py': - continue - if not os.path.exists(filename): - continue - warnings += check_file(path=filename, ignore=ignore, - complexity=complexity) - - if strict: - return warnings - - return 0 - - -def hg_hook(ui, repo, **kwargs): - _initpep8() - complexity = ui.config('flake8', 'complexity', default=-1) - warnings = 0 - - for file_ in _get_files(repo, **kwargs): - warnings += check_file(file_, complexity) - - strict = ui.configbool('flake8', 'strict', default=True) - - if strict: - return warnings - - return 0 - - -try: - from setuptools import Command -except ImportError: - Flake8Command = None -else: - class Flake8Command(Command): - description = "Run flake8 on modules registered in setuptools" - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def distribution_files(self): - if self.distribution.packages: - for package in self.distribution.packages: - yield package.replace(".", os.path.sep) - - if self.distribution.py_modules: - for filename in self.distribution.py_modules: - yield "%s.py" % filename - - def run(self): - _initpep8() - - # _get_python_files can produce the same file several - # times, if one of its paths is a parent of another. Keep - # a set of checked files to de-duplicate. - checked = set() - - warnings = 0 - for path in _get_python_files(self.distribution_files()): - if path not in checked: - warnings += check_file(path) - checked.add(path) - - raise SystemExit(warnings > 0) +from flake8.hooks import git_hook, hg_hook # noqa +from flake8.main import main +from flake8.main import (check_code, check_file, get_parser, # noqa + Flake8Command) # noqa if __name__ == '__main__': diff --git a/flake8/util.py b/flake8/util.py index c9a6a75..1323857 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -2,6 +2,8 @@ from __future__ import with_statement import re import os +pep8style = None + def skip_warning(warning, ignore=[]): # XXX quick dirty hack, just need to keep the line in the warning @@ -40,3 +42,18 @@ def skip_file(path): finally: f.close() return _NOQA.search(content) is not None + + +def _initpep8(): + # default pep8 setup + global pep8style + import pep8 + if pep8style is None: + pep8style = pep8.StyleGuide(config_file=True) + pep8style.options.physical_checks = pep8.find_checks('physical_line') + pep8style.options.logical_checks = pep8.find_checks('logical_line') + pep8style.options.counters = dict.fromkeys(pep8.BENCHMARK_KEYS, 0) + pep8style.options.messages = {} + pep8style.options.max_line_length = 79 + pep8style.args = [] + return pep8style From d8a30e19af37aa5295a551cf516ddf43065bed65 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 2 Jan 2013 16:04:55 -0500 Subject: [PATCH 10/20] Mention changes in preparation for 2.0.0 Working version of --install-hook. --- README => README.rst | 16 +++++++++-- flake8/hooks.py | 66 +++++++++++++++++++++++++++++++++++++++++++- flake8/main.py | 40 ++++----------------------- flake8/util.py | 51 ++++++++++++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 135 insertions(+), 40 deletions(-) rename README => README.rst (96%) diff --git a/README b/README.rst similarity index 96% rename from README rename to README.rst index 2c08bf7..acd1a6e 100644 --- a/README +++ b/README.rst @@ -151,7 +151,7 @@ files listed in your ``py_modules`` and ``packages``. If any warnings are found, the command will exit with an error code:: $ python setup.py flake8 - + Original projects @@ -160,8 +160,9 @@ Original projects Flake8 is just a glue project, all the merits go to the creators of the original projects: -- pep8: http://github.com/jcrocholl/pep8/ +- pep8: https://github.com/jcrocholl/pep8/ - PyFlakes: http://divmod.org/trac/wiki/DivmodPyflakes +- flakey: https://bitbucket.org/icordasc/flakey - McCabe: http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html Warning / Error codes @@ -207,7 +208,7 @@ pep8: - W603: '<>' is deprecated, use '!=' - W604: backticks are deprecated, use 'repr()' -pyflakes: +flakey: - W402: imported but unused - W403: import from line shadowed by loop variable @@ -228,6 +229,15 @@ McCabe: CHANGES ======= +2.0.0 - 2013-01-xx +------------------ + +- Fixes #13: pep8 and flakey are now external dependencies +- Split run.py into main.py and hooks.py for better logic +- Expose our parser for our users +- New feature: Install git and hg hooks automagically +- By relying on flakey, we also fixed #45 and #35 + 1.7.0 - 2012-12-21 ------------------ diff --git a/flake8/hooks.py b/flake8/hooks.py index 970fffa..7d493b8 100644 --- a/flake8/hooks.py +++ b/flake8/hooks.py @@ -1,5 +1,7 @@ import os -from flake8.util import _initpep8, pep8style, skip_file +import sys +from flake8.util import (_initpep8, pep8style, skip_file, get_parser, + ConfigParser) from subprocess import Popen, PIPE @@ -78,3 +80,65 @@ def find_vcs(): elif os.path.isdir('.hg'): return '.hg/hgrc' return '' + + +git_hook_file = """#!/usr/bin/env python +import sys +import os +from flake8.hooks import git_hook + +COMPLEXITY = os.getenv('FLAKE8_COMPLEXITY', 10) +STRICT = os.getenv('FLAKE8_STRICT', False) + + +if __name__ == '__main__': + sys.exit(git_hook(complexity=COMPLEXITY, strict=STRICT)) +""" + + +def _install_hg_hook(path): + c = ConfigParser() + c.readfp(open(path, 'r')) + if not c.has_section('hooks'): + c.add_section('hooks') + + if not c.has_option('hooks', 'commit'): + c.set('hooks', 'commit', 'python:flake8.hooks.hg_hook') + + if not c.has_option('hooks', 'qrefresh'): + c.set('hooks', 'qrefresh', 'python:flake8.hooks.hg_hook') + + if not c.has_section('flake8'): + c.add_section('flake8') + + if not c.has_option('flake8', 'complexity'): + c.set('flake8', 'complexity', str(os.getenv('FLAKE8_COMPLEXITY', 10))) + + if not c.has_option('flake8', 'strict'): + c.set('flake8', 'strict', os.getenv('FLAKE8_STRICT', False)) + + c.write(open(path, 'w+')) + + +def install_hook(): + vcs = find_vcs() + + if not vcs: + p = get_parser() + sys.stderr.write('Error: could not find either a git or mercurial ' + 'directory. Please re-run this in a proper ' + 'repository.') + p.print_help() + sys.exit(1) + + status = 0 + if 'git' in vcs: + with open(vcs, 'w+') as fd: + fd.write(git_hook_file) + os.chmod(vcs, 744) + elif 'hg' in vcs: + _install_hg_hook(vcs) + else: + status = 1 + + sys.exit(status) diff --git a/flake8/main.py b/flake8/main.py index df92450..4cb8969 100644 --- a/flake8/main.py +++ b/flake8/main.py @@ -3,42 +3,8 @@ import sys import pep8 import flakey import select -import optparse from flake8 import mccabe -from flake8 import __version__ -from flake8.util import _initpep8, skip_file - -pep8style = None - - -def get_parser(): - """Create a custom OptionParser""" - - def version(option, opt, value, parser): - parser.print_usage() - parser.print_version() - sys.exit(0) - - # Create our own parser - parser = optparse.OptionParser('%prog [options] [file.py|directory]', - version=version) - parser.version = '{0} (pep8: {1}, flakey: {2})'.format( - __version__, pep8.__version__, flakey.__version__) - parser.remove_option('--version') - # don't overlap with pep8's verbose option - parser.add_option('--builtins', default='', dest='builtins', - help="append builtin functions to flakey's " - "_MAGIC_BUILTINS") - parser.add_option('--ignore', default='', - help='skip errors and warnings (e.g. E4,W)') - parser.add_option('--exit-zero', action='store_true', default=False, - help='Exit with status 0 even if there are errors') - parser.add_option('--max-complexity', default=-1, action='store', - type='int', help='McCabe complexity threshold') - parser.add_option('-V', '--version', action='callback', - callback=version, - help='Print the version info for flake8') - return parser +from flake8.util import _initpep8, skip_file, get_parser, pep8style def main(): @@ -48,6 +14,10 @@ def main(): parser = get_parser() opts, sys.argv = parser.parse_args() + if opts.install_hook: + from flake8.hooks import install_hook + install_hook() + # make sure pep8 gets the information it expects sys.argv.insert(0, 'pep8') diff --git a/flake8/util.py b/flake8/util.py index 1323857..17ce56f 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -1,10 +1,61 @@ from __future__ import with_statement import re import os +import optparse + +try: + # Python 2 + from ConfigParser import ConfigParser +except ImportError: + # Python 3 + from configparser import ConfigParser pep8style = None +def get_parser(): + """Create a custom OptionParser""" + from flake8 import __version__ + import flakey + import pep8 + + def version(option, opt, value, parser): + parser.print_usage() + parser.print_version() + sys.exit(0) + + def help(option, opt, value, parser): + parser.print_help() + p = pep8.get_parser() + p.print_help() + + # Create our own parser + parser = optparse.OptionParser('%prog [options] [file.py|directory]', + version=version, add_help_option=False) + parser.version = '{0} (pep8: {1}, flakey: {2})'.format( + __version__, pep8.__version__, flakey.__version__) + parser.remove_option('--version') + # don't overlap with pep8's verbose option + parser.add_option('--builtins', default='', dest='builtins', + help="append builtin functions to flakey's " + "_MAGIC_BUILTINS") + parser.add_option('--ignore', default='', + help='skip errors and warnings (e.g. E4,W)') + parser.add_option('--exit-zero', action='store_true', default=False, + help='Exit with status 0 even if there are errors') + parser.add_option('--max-complexity', default=-1, action='store', + type='int', help='McCabe complexity threshold') + parser.add_option('-V', '--version', action='callback', + callback=version, + help='Print the version info for flake8') + parser.add_option('--install-hook', default=False, action='store_true', + help='Install the appropriate hook for this ' + 'repository.', dest='install_hook') + parser.add_option('-h', '--help', action='callback', callback=help, + help='Print this message and exit') + return parser + + def skip_warning(warning, ignore=[]): # XXX quick dirty hack, just need to keep the line in the warning if not hasattr(warning, 'message'): diff --git a/setup.py b/setup.py index 0cb9cee..0b6360d 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ else: from flake8 import __version__ -README = open('README').read() +README = open('README.rst').read() setup( name="flake8", From 9cd5dbc96f5205f2d8912434cc910a09004f30fe Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Fri, 4 Jan 2013 11:23:47 -0500 Subject: [PATCH 11/20] Rely on flakey's new ability to skip stdin errors Resolves our half of tickets #53, and #53 I believe. --- flake8/main.py | 4 ++-- flake8/util.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/flake8/main.py b/flake8/main.py index df92450..2b7dc94 100644 --- a/flake8/main.py +++ b/flake8/main.py @@ -92,8 +92,8 @@ def check_file(path, ignore=(), complexity=-1): def check_code(code, ignore=(), complexity=-1): - warning = flakey.check(code, 'stdin') - warnings = flakey.print_messages(warning, ignore=ignore) + warning = flakey.check(code, '') + warnings = flakey.print_messages(warning, ignore=ignore, code=code) warnings += pep8style.input_file(None, lines=code.split('\n')) if complexity > -1: warnings += mccabe.get_code_complexity(code, complexity) diff --git a/flake8/util.py b/flake8/util.py index 1323857..b20b0bf 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -1,6 +1,7 @@ from __future__ import with_statement import re import os +from io import StringIO pep8style = None @@ -29,14 +30,18 @@ def skip_line(line): _NOQA = re.compile(r'flake8[:=]\s*noqa', re.I | re.M) -def skip_file(path): +def skip_file(path, source=None): """Returns True if this header is found in path # flake8: noqa """ - if not os.path.isfile(path): + if os.path.isfile(path): + f = open(path) + elif source: + f = StringIO(source) + else: return False - f = open(path) + try: content = f.read() finally: From 93538ea852ca830789b250a3008deba7b2db6949 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Fri, 4 Jan 2013 12:20:42 -0500 Subject: [PATCH 12/20] Add myself as a maintainer in setup() --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 0b6360d..411a480 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,8 @@ setup( description="code checking using pep8 and pyflakes", author="Tarek Ziade", author_email="tarek@ziade.org", + maintainer="Ian Cordasco", + maintainer_email="graffatcolmingov@gmail.com", url="http://bitbucket.org/tarek/flake8", packages=["flake8", "flake8.tests"], scripts=scripts, From 264bc0370ac3fc82f22b0f370c91bd0a12603c10 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Sat, 5 Jan 2013 21:23:44 -0500 Subject: [PATCH 13/20] Use pep8's parser That much closer to a release --- flake8/main.py | 25 ++++++++++++++++++++----- flake8/util.py | 22 ++++++---------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/flake8/main.py b/flake8/main.py index a438d9b..9e462aa 100644 --- a/flake8/main.py +++ b/flake8/main.py @@ -4,30 +4,45 @@ import pep8 import flakey import select from flake8 import mccabe -from flake8.util import _initpep8, skip_file, get_parser, pep8style +from flake8.util import _initpep8, skip_file, get_parser + +pep8style = None def main(): global pep8style - # parse out our flags so pep8 doesn't get confused parser = get_parser() - opts, sys.argv = parser.parse_args() + opts, _ = parser.parse_args() if opts.install_hook: from flake8.hooks import install_hook install_hook() + if opts.builtins: + s = '--builtins={0}'.format(opts.builtins) + sys.argv.remove(s) + + if opts.exit_zero: + sys.argv.remove('--exit-zero') + + if opts.install_hook: + sys.argv.remove('--install-hook') + + complexity = opts.max_complexity + if complexity > 0: + sys.argv.remove('--max-complexity={0}'.format(complexity)) + # make sure pep8 gets the information it expects + sys.argv.pop(0) sys.argv.insert(0, 'pep8') pep8style = pep8.StyleGuide(parse_argv=True, config_file=True) options = pep8style.options - complexity = opts.max_complexity - builtins = set(opts.builtins.split(',')) warnings = 0 stdin = None + builtins = set(opts.builtins.split(',')) if builtins: orig_builtins = set(flakey.checker._MAGIC_GLOBALS) flakey.checker._MAGIC_GLOBALS = orig_builtins | builtins diff --git a/flake8/util.py b/flake8/util.py index 2c20516..49042d6 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -1,6 +1,7 @@ from __future__ import with_statement import re import os +import sys from io import StringIO import optparse @@ -19,41 +20,30 @@ def get_parser(): from flake8 import __version__ import flakey import pep8 + parser = pep8.get_parser() def version(option, opt, value, parser): parser.print_usage() parser.print_version() sys.exit(0) - def help(option, opt, value, parser): - parser.print_help() - p = pep8.get_parser() - p.print_help() - - # Create our own parser - parser = optparse.OptionParser('%prog [options] [file.py|directory]', - version=version, add_help_option=False) parser.version = '{0} (pep8: {1}, flakey: {2})'.format( __version__, pep8.__version__, flakey.__version__) parser.remove_option('--version') - # don't overlap with pep8's verbose option parser.add_option('--builtins', default='', dest='builtins', help="append builtin functions to flakey's " "_MAGIC_BUILTINS") - parser.add_option('--ignore', default='', - help='skip errors and warnings (e.g. E4,W)') parser.add_option('--exit-zero', action='store_true', default=False, help='Exit with status 0 even if there are errors') parser.add_option('--max-complexity', default=-1, action='store', type='int', help='McCabe complexity threshold') - parser.add_option('-V', '--version', action='callback', - callback=version, - help='Print the version info for flake8') parser.add_option('--install-hook', default=False, action='store_true', help='Install the appropriate hook for this ' 'repository.', dest='install_hook') - parser.add_option('-h', '--help', action='callback', callback=help, - help='Print this message and exit') + # don't overlap with pep8's verbose option + parser.add_option('-V', '--version', action='callback', + callback=version, + help='Print the version info for flake8') return parser From 0d3cc25400c771d3f7f24ef8bb3e6d585852b5c6 Mon Sep 17 00:00:00 2001 From: John Watson Date: Tue, 8 Jan 2013 20:33:50 -0800 Subject: [PATCH 14/20] Clear os.walk's dirnames to properly exclude dirs I have a project: `./foo/__init__.py` `./foo/bar/__init__.py` `./foo/baz.py` `./__init__.py` `./qux.py` Running: `flake8 --exclude="foo" ./` Checks: `./foo/bar/__init__.py` `./__init__.py` `./qux.py` This patch prevents the continued walking of **foo**'s subdirectories and not just files. --- flake8/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flake8/main.py b/flake8/main.py index 9e462aa..faac93b 100644 --- a/flake8/main.py +++ b/flake8/main.py @@ -141,6 +141,7 @@ def _get_python_files(paths): if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path): if pep8style.excluded(dirpath): + dirnames[:] = [] continue for filename in filenames: if not filename.endswith('.py'): From 8eec1e5e116feaaa1587a37b5974d664728fff51 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 9 Jan 2013 11:26:00 -0500 Subject: [PATCH 15/20] Fixes #57 Don't allow for ignore to be None --- flake8/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flake8/util.py b/flake8/util.py index 49042d6..c8188ce 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -49,8 +49,9 @@ def get_parser(): def skip_warning(warning, ignore=[]): # XXX quick dirty hack, just need to keep the line in the warning - if not hasattr(warning, 'message'): + if not hasattr(warning, 'message') or ignore is None: # McCabe's warnings cannot be skipped afaik, and they're all strings. + # And we'll get a TypeError otherwise return False if warning.message.split()[0] in ignore: return True From e236b7f6b33590b49b87462b199d16fee2e8fd55 Mon Sep 17 00:00:00 2001 From: Kevin Stanton Date: Wed, 9 Jan 2013 11:31:41 -0500 Subject: [PATCH 16/20] Add support for the pep8 config file Closes #55. --- flake8/hooks.py | 3 ++- flake8/util.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/flake8/hooks.py b/flake8/hooks.py index 7d493b8..a99bae3 100644 --- a/flake8/hooks.py +++ b/flake8/hooks.py @@ -35,8 +35,9 @@ def git_hook(complexity=-1, strict=False, ignore=None, lazy=False): def hg_hook(ui, repo, **kwargs): from flake8.main import check_file - _initpep8() complexity = ui.config('flake8', 'complexity', default=-1) + config = ui.config('flake8', 'config', default=True) + _initpep8(config_file=config) warnings = 0 for file_ in _get_files(repo, **kwargs): diff --git a/flake8/util.py b/flake8/util.py index c8188ce..8d78edb 100644 --- a/flake8/util.py +++ b/flake8/util.py @@ -91,16 +91,17 @@ def skip_file(path, source=None): return _NOQA.search(content) is not None -def _initpep8(): +def _initpep8(config_file=True): # default pep8 setup global pep8style import pep8 if pep8style is None: - pep8style = pep8.StyleGuide(config_file=True) + pep8style = pep8.StyleGuide(config_file=config_file) pep8style.options.physical_checks = pep8.find_checks('physical_line') pep8style.options.logical_checks = pep8.find_checks('logical_line') pep8style.options.counters = dict.fromkeys(pep8.BENCHMARK_KEYS, 0) pep8style.options.messages = {} - pep8style.options.max_line_length = 79 + if not pep8style.options.max_line_length: + pep8style.options.max_line_length = 79 pep8style.args = [] return pep8style From e9366a904ee05844b33e0858075018aa6652cd6e Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 9 Jan 2013 13:20:18 -0500 Subject: [PATCH 17/20] Closes #54 This was really the only major difference and it worked. --- flake8/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake8/main.py b/flake8/main.py index 9e462aa..1241483 100644 --- a/flake8/main.py +++ b/flake8/main.py @@ -79,7 +79,7 @@ def check_file(path, ignore=(), complexity=-1): def check_code(code, ignore=(), complexity=-1): warning = flakey.check(code, '') warnings = flakey.print_messages(warning, ignore=ignore, code=code) - warnings += pep8style.input_file(None, lines=code.split('\n')) + warnings += pep8style.input_file('-', lines=code.split('\n')) if complexity > -1: warnings += mccabe.get_code_complexity(code, complexity) return warnings From f06f484b21fa2415aeb2ffcefb2cdaa5151d2e90 Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Fri, 11 Jan 2013 10:09:08 -0500 Subject: [PATCH 18/20] Some changes re #59 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 411a480..e8f0e0c 100644 --- a/setup.py +++ b/setup.py @@ -15,8 +15,8 @@ else: from setuptools import setup # NOQA kwargs = { 'entry_points': { - 'distutils.commands': ['flake8 = flake8.run:Flake8Command'], - 'console_scripts': ['flake8 = flake8.run:main'] + 'distutils.commands': ['flake8 = flake8.main:Flake8Command'], + 'console_scripts': ['flake8 = flake8.main:main'] }, 'tests_require': ['nose'], 'test_suite': 'nose.collector', @@ -42,7 +42,7 @@ setup( url="http://bitbucket.org/tarek/flake8", packages=["flake8", "flake8.tests"], scripts=scripts, - requires=["flakey (>=2.0)", "pep8 (>=1.4)"], + install_requires=["flakey (==2.0)", "pep8 (>=1.4)"], long_description=README, classifiers=[ "Environment :: Console", From 2d2744b017d98b75564ad060db449b2c8a2a032c Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Fri, 18 Jan 2013 19:23:36 -0500 Subject: [PATCH 19/20] Pin pep8 dependecy. Thanks @flox (@florentx)! --- flake8/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flake8/__init__.py b/flake8/__init__.py index 2a9b184..a3332a5 100644 --- a/flake8/__init__.py +++ b/flake8/__init__.py @@ -1,2 +1,2 @@ -__version__ = '1.7.0' +__version__ = '2.0' diff --git a/setup.py b/setup.py index e8f0e0c..8321794 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ setup( url="http://bitbucket.org/tarek/flake8", packages=["flake8", "flake8.tests"], scripts=scripts, - install_requires=["flakey (==2.0)", "pep8 (>=1.4)"], + install_requires=["flakey (==2.0)", "pep8 (==1.4.1)"], long_description=README, classifiers=[ "Environment :: Console", From ce7952497eab50a9cbd31805e35f1a36037cfa7c Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Tue, 22 Jan 2013 09:29:07 -0500 Subject: [PATCH 20/20] Ignore .orig files --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index 41cb134..0343796 100644 --- a/.hgignore +++ b/.hgignore @@ -8,3 +8,4 @@ man \.Python nose* .*\.swp +.*.orig