From a04d82c53cb858a9cd2a2180be7d28aac8766ed6 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Mon, 8 Apr 2013 14:47:15 +0200 Subject: [PATCH 001/582] Set commentstring in pymode_options. This setting is used in vanilla vim for adding fold markers, but also by vim-commentary for using the appropriate comment marker for specific filetypes. --- ftplugin/python/pymode.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index ffcf0803..2ca6f33e 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -26,6 +26,7 @@ if pymode#Option('options') endif setlocal nowrap setlocal textwidth=79 + setlocal commentstring=#%s endif " }}} From 5c308c24069fa7d27123aafbb037a94d0b3b7db6 Mon Sep 17 00:00:00 2001 From: Denis Kasak Date: Mon, 8 Apr 2013 14:51:22 +0200 Subject: [PATCH 002/582] Update documentation regarding commentstring. --- doc/pymode.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/pymode.txt b/doc/pymode.txt index c4b19c3f..f37796f5 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -371,6 +371,7 @@ buffers: > setlocal number setlocal nowrap setlocal textwidth=80 + setlocal commentstring=#%s < ------------------------------------------------------------------------------ *'pymode_motion'* From dd880f273c7f6d0ea9bd0e1a4c88521abbfb3b1f Mon Sep 17 00:00:00 2001 From: ikame Date: Tue, 9 Apr 2013 12:03:35 +0200 Subject: [PATCH 003/582] keyword match pythonClass --- syntax/python.vim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syntax/python.vim b/syntax/python.vim index ba2b9f92..7a8d06d3 100644 --- a/syntax/python.vim +++ b/syntax/python.vim @@ -24,8 +24,10 @@ call pymode#Default('g:pymode_syntax_all', 1) syn keyword pythonStatement global assert syn keyword pythonStatement lambda yield syn keyword pythonStatement with as - syn keyword pythonStatement def class nextgroup=pythonFunction skipwhite + syn keyword pythonStatement def nextgroup=pythonFunction skipwhite syn match pythonFunction "[a-zA-Z_][a-zA-Z0-9_]*" display contained + syn keyword pythonStatement class nextgroup=pythonClass skipwhite + syn match pythonClass "[a-zA-Z_][a-zA-Z0-9_]*" display contained syn keyword pythonRepeat for while syn keyword pythonConditional if elif else syn keyword pythonInclude import from From 0dd203fa89d05d35d6b84399662228649a938bce Mon Sep 17 00:00:00 2001 From: lee Date: Sat, 13 Apr 2013 20:04:52 +0800 Subject: [PATCH 004/582] fix Chinese runtime error fix Chinese runtime error --- autoload/pymode/run.vim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index f4bcab8b..df971178 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -31,7 +31,7 @@ fun! pymode#run#Run(line1, line2) "{{{ python << EOF if out: vim.command("call pymode#TempBuffer()") - vim.current.buffer.append([x.encode(enc) for x in out.split('\n')], 0) + vim.current.buffer.append([x.decode("utf-8").encode(enc) for x in out.split('\n')], 0) vim.command("wincmd p") else: vim.command('call pymode#WideMessage("No output.")') From f82867b8e0b5a9d406fb64a1297eb7a0edbce9a7 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 3 May 2013 18:30:38 +0800 Subject: [PATCH 005/582] Update pylama --- Changelog.rst | 5 + pylibs/pylama/__init__.py | 2 +- pylibs/pylama/mccabe.py | 2 + pylibs/pylama/pep8.py | 60 +++-- pylibs/pylama/pyflakes/__init__.py | 2 +- pylibs/pylama/pyflakes/checker.py | 255 +++++++++++++----- pylibs/pylama/pyflakes/messages.py | 105 ++++---- pylibs/pylama/pylint/__pkginfo__.py | 8 +- pylibs/pylama/pylint/checkers/__init__.py | 2 +- pylibs/pylama/pylint/checkers/base.py | 65 ++++- pylibs/pylama/pylint/checkers/format.py | 121 +-------- pylibs/pylama/pylint/checkers/imports.py | 31 +-- pylibs/pylama/pylint/checkers/logging.py | 6 +- .../checkers/{string_format.py => strings.py} | 141 +++++++++- pylibs/pylama/pylint/checkers/typecheck.py | 2 +- pylibs/pylama/pylint/checkers/utils.py | 3 + pylibs/pylama/pylint/lint.py | 30 ++- .../pylama/pylint/logilab/astng/__init__.py | 8 +- .../pylint/logilab/astng/__pkginfo__.py | 6 +- .../pylama/pylint/logilab/astng/as_string.py | 82 ++++-- pylibs/pylama/pylint/logilab/astng/bases.py | 82 ++---- .../pylint/logilab/astng/brain/py2stdlib.py | 8 +- pylibs/pylama/pylint/logilab/astng/builder.py | 9 +- .../pylama/pylint/logilab/astng/exceptions.py | 16 +- .../pylama/pylint/logilab/astng/inference.py | 35 +-- pylibs/pylama/pylint/logilab/astng/manager.py | 7 +- pylibs/pylama/pylint/logilab/astng/mixins.py | 18 +- .../pylint/logilab/astng/node_classes.py | 44 ++- pylibs/pylama/pylint/logilab/astng/nodes.py | 4 +- .../pylama/pylint/logilab/astng/protocols.py | 6 +- .../pylint/logilab/astng/raw_building.py | 24 +- .../pylama/pylint/logilab/astng/rebuilder.py | 5 +- .../pylint/logilab/astng/scoped_nodes.py | 42 ++- pylibs/pylama/pylint/logilab/astng/utils.py | 9 +- pylibs/pylama/pylint/reporters/__init__.py | 5 +- pylibs/pylama/pylint/reporters/text.py | 4 +- pylibs/pylama/pylint/utils.py | 46 +++- pylibs/pylama/utils.py | 2 +- 38 files changed, 760 insertions(+), 542 deletions(-) rename pylibs/pylama/pylint/checkers/{string_format.py => strings.py} (56%) diff --git a/Changelog.rst b/Changelog.rst index 68cf7a56..a9984140 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +## 2013-05-03 0.6.17 +-------------------- +* Update `Pylint` to version 0.28.0; +* Update `pyflakes` to version 0.7.3; + ## 2013-04-26 0.6.16 -------------------- * Improvement folding (thanks @alvinfrancis); diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 28159700..cebb3212 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,6 +1,6 @@ " pylama -- Python code audit. " -version_info = (0, 3, 0) +version_info = (0, 3, 1) __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/mccabe.py b/pylibs/pylama/mccabe.py index b52671bc..432f4c1e 100644 --- a/pylibs/pylama/mccabe.py +++ b/pylibs/pylama/mccabe.py @@ -310,3 +310,5 @@ def main(argv): if __name__ == '__main__': main(sys.argv[1:]) + +# lint=0 diff --git a/pylibs/pylama/pep8.py b/pylibs/pylama/pep8.py index 888df59d..e586c1d7 100644 --- a/pylibs/pylama/pep8.py +++ b/pylibs/pylama/pep8.py @@ -457,17 +457,17 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): # 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") + close_bracket = (token_type == tokenize.OP and text in ']})') + visual_indent = not close_bracket and indent_chances.get(start[1]) + + if close_bracket and indent[depth]: + # closing bracket for visual indent + if start[1] != indent[depth]: + yield (start, "E124 closing bracket does not match " + "visual indentation") + elif close_bracket and not hang: + # closing bracket matches indentation of opening bracket's line + pass elif visual_indent is True: # visual indent is verified if not indent[depth]: @@ -481,7 +481,9 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): "under-indented for visual indent") elif hang == 4 or (indent_next and rel_indent[row] == 8): # hanging indent is verified - pass + if close_bracket: + yield (start, "E123 closing bracket does not match " + "indentation of opening bracket's line") else: # indent is broken if hang <= 0: @@ -1015,6 +1017,7 @@ def readlines(filename): finally: f.close() + BOM_UTF8 = '\xef\xbb\xbf' isidentifier = re.compile(r'[a-zA-Z_]\w*').match stdin_get_value = sys.stdin.read else: @@ -1033,6 +1036,7 @@ def readlines(filename): finally: f.close() + BOM_UTF8 = '\ufeff' isidentifier = str.isidentifier def stdin_get_value(): @@ -1198,14 +1202,19 @@ def __init__(self, filename=None, lines=None, self.lines = [] else: self.lines = lines + if self.lines and self.lines[0].startswith(BOM_UTF8): + self.lines[0] = self.lines[0][len(BOM_UTF8):] self.report = report or options.report self.report_error = self.report.error def report_invalid_syntax(self): exc_type, exc = sys.exc_info()[:2] - offset = exc.args[1] - if len(offset) > 2: - offset = offset[1:3] + if len(exc.args) > 1: + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + else: + offset = (1, 0) self.report_error(offset[0], offset[1] or 0, 'E901 %s: %s' % (exc_type.__name__, exc.args[0]), self.report_invalid_syntax) @@ -1322,7 +1331,7 @@ def check_logical(self): def check_ast(self): try: tree = compile(''.join(self.lines), '', 'exec', PyCF_ONLY_AST) - except SyntaxError: + except (SyntaxError, TypeError): return self.report_invalid_syntax() for name, cls, _ in self._ast_checks: checker = cls(tree, self.filename) @@ -1579,7 +1588,7 @@ def __init__(self, *args, **kwargs): options.ignore = tuple(DEFAULT_IGNORE.split(',')) else: # Ignore all checks which are not explicitly selected - options.ignore = tuple(options.ignore or options.select and ('',)) + options.ignore = ('',) if options.select else tuple(options.ignore) options.benchmark_keys = BENCHMARK_KEYS[:] options.ignore_code = self.ignore_code options.physical_checks = self.get_checks('physical_line') @@ -1632,23 +1641,26 @@ def input_dir(self, dirname): print('directory ' + root) counters['directories'] += 1 for subdir in sorted(dirs): - if self.excluded(os.path.join(root, subdir)): + if self.excluded(subdir, root): dirs.remove(subdir) for filename in sorted(files): # contain a pattern that matches? if ((filename_match(filename, filepatterns) and - not self.excluded(filename))): + not self.excluded(filename, root))): runner(os.path.join(root, filename)) - def excluded(self, filename): + def excluded(self, filename, parent=None): """ Check if options.exclude contains a pattern that matches filename. """ + if not self.options.exclude: + return False basename = os.path.basename(filename) - return any((filename_match(filename, self.options.exclude, - default=False), - filename_match(basename, self.options.exclude, - default=False))) + if filename_match(basename, self.options.exclude): + return True + if parent: + filename = os.path.join(parent, filename) + return filename_match(filename, self.options.exclude) def ignore_code(self, code): """ diff --git a/pylibs/pylama/pyflakes/__init__.py b/pylibs/pylama/pyflakes/__init__.py index 7a2d0cd1..ca95838a 100644 --- a/pylibs/pylama/pyflakes/__init__.py +++ b/pylibs/pylama/pyflakes/__init__.py @@ -1,2 +1,2 @@ -__version__ = '0.6.1' +__version__ = '0.7.3a0' diff --git a/pylibs/pylama/pyflakes/checker.py b/pylibs/pylama/pyflakes/checker.py index 7e5286e2..3d4c6475 100644 --- a/pylibs/pylama/pyflakes/checker.py +++ b/pylibs/pylama/pyflakes/checker.py @@ -1,13 +1,17 @@ -# -*- test-case-name: pyflakes -*- -# (c) 2005-2010 Divmod, Inc. -# See LICENSE file for details - -import os.path +""" +Main module. + +Implement the central Checker class. +Also, it models the Bindings and Scopes. +""" +import doctest +import os +import sys try: - import builtins + builtin_vars = dir(__import__('builtins')) PY2 = False except ImportError: - import __builtin__ as builtins + builtin_vars = dir(__import__('__builtin__')) PY2 = True try: @@ -44,6 +48,15 @@ def iter_child_nodes(node): from . import messages +if PY2: + def getNodeType(node_class): + # workaround str.upper() which is locale-dependent + return str(unicode(node_class.__name__).upper()) +else: + def getNodeType(node_class): + return node_class.__name__.upper() + + class Binding(object): """ Represents the binding of a value to a name. @@ -71,10 +84,6 @@ def __repr__(self): id(self)) -class UnBinding(Binding): - """Created by the 'del' operator.""" - - class Importation(Binding): """ A binding created by an import statement. @@ -147,10 +156,10 @@ def names(self): class Scope(dict): importStarred = False # set to True when import * is found - usesLocals = False def __repr__(self): - return '<%s at 0x%x %s>' % (self.__class__.__name__, id(self), dict.__repr__(self)) + scope_cls = self.__class__.__name__ + return '<%s at 0x%x %s>' % (scope_cls, id(self), dict.__repr__(self)) class ClassScope(Scope): @@ -163,9 +172,24 @@ class FunctionScope(Scope): @ivar globals: Names declared 'global' in this function. """ + usesLocals = False + alwaysUsed = set(['__tracebackhide__', + '__traceback_info__', '__traceback_supplement__']) + def __init__(self): super(FunctionScope, self).__init__() - self.globals = {} + # Simplify: manage the special locals as globals + self.globals = self.alwaysUsed.copy() + + def unusedAssignments(self): + """ + Return a generator for the assignments which have not been used. + """ + for name, binding in self.items(): + if (not binding.used and name not in self.globals + and not self.usesLocals + and isinstance(binding, Assignment)): + yield name, binding class ModuleScope(Scope): @@ -199,10 +223,18 @@ class Checker(object): """ nodeDepth = 0 + offset = None traceTree = False - builtIns = set(dir(builtins)) | set(_MAGIC_GLOBALS) + withDoctest = ('PYFLAKES_NODOCTEST' not in os.environ) + + builtIns = set(builtin_vars).union(_MAGIC_GLOBALS) + _customBuiltIns = os.environ.get('PYFLAKES_BUILTINS') + if _customBuiltIns: + builtIns.update(_customBuiltIns.split(',')) + del _customBuiltIns def __init__(self, tree, filename='(none)', builtins=None): + self._nodeHandlers = {} self._deferredFunctions = [] self._deferredAssignments = [] self.deadScopes = [] @@ -211,6 +243,7 @@ def __init__(self, tree, filename='(none)', builtins=None): if builtins: self.builtIns = self.builtIns.union(builtins) self.scopeStack = [ModuleScope()] + self.exceptHandlers = [()] self.futuresAllowed = True self.root = tree self.handleChildren(tree) @@ -235,21 +268,22 @@ def deferFunction(self, callable): `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[:])) + self._deferredFunctions.append((callable, self.scopeStack[:], self.offset)) def deferAssignment(self, callable): """ Schedule an assignment handler to be called just after deferred function handlers. """ - self._deferredAssignments.append((callable, self.scopeStack[:])) + self._deferredAssignments.append((callable, self.scopeStack[:], self.offset)) def runDeferred(self, deferred): """ Run the callables in C{deferred} using their associated scope stack. """ - for handler, scope in deferred: + for handler, scope, offset in deferred: self.scopeStack = scope + self.offset = offset handler() @property @@ -268,12 +302,13 @@ def checkDeadScopes(self): export = isinstance(scope.get('__all__'), ExportBinding) if export: all = scope['__all__'].names() - if not scope.importStarred and os.path.basename(self.filename) != '__init__.py': + if not scope.importStarred and \ + os.path.basename(self.filename) != '__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) + scope['__all__'].source, name) else: all = [] @@ -282,7 +317,7 @@ def checkDeadScopes(self): if isinstance(importation, Importation): if not importation.used and importation.name not in all: self.report(messages.UnusedImport, - importation.source.lineno, importation.name) + importation.source, importation.name) def pushFunctionScope(self): self.scopeStack.append(FunctionScope()) @@ -358,12 +393,13 @@ def addBinding(self, node, value, reportRedef=True): existing = scope.get(value.name) if (isinstance(existing, Importation) and not existing.used - and (not isinstance(value, Importation) or value.fullName == existing.fullName) + and (not isinstance(value, Importation) or + value.fullName == existing.fullName) and reportRedef and not self.differentForks(node, existing.source)): redefinedWhileUnused = True self.report(messages.RedefinedWhileUnused, - node.lineno, value.name, existing.source.lineno) + node, value.name, existing.source) existing = self.scope.get(value.name) if not redefinedWhileUnused and self.hasParent(value.source, ast.ListComp): @@ -371,41 +407,44 @@ def addBinding(self, node, value, reportRedef=True): and not self.hasParent(existing.source, (ast.For, ast.ListComp)) and not self.differentForks(node, existing.source)): self.report(messages.RedefinedInListComp, - node.lineno, value.name, existing.source.lineno) + node, value.name, existing.source) - if isinstance(value, UnBinding): - try: - del self.scope[value.name] - except KeyError: - self.report(messages.UndefinedName, node.lineno, value.name) - elif (isinstance(existing, Definition) - and not existing.used - and not self.differentForks(node, existing.source)): + if (isinstance(existing, Definition) + and not existing.used + and not self.differentForks(node, existing.source)): self.report(messages.RedefinedWhileUnused, - node.lineno, value.name, existing.source.lineno) + node, value.name, existing.source) else: self.scope[value.name] = value + def getNodeHandler(self, node_class): + try: + return self._nodeHandlers[node_class] + except KeyError: + nodeType = getNodeType(node_class) + self._nodeHandlers[node_class] = handler = getattr(self, nodeType) + return handler + def handleNodeLoad(self, node): name = getNodeName(node) if not name: return # try local scope - importStarred = self.scope.importStarred try: - self.scope[name].used = (self.scope, node.lineno) + self.scope[name].used = (self.scope, node) except KeyError: pass else: return # try enclosing function scopes + importStarred = self.scope.importStarred for scope in self.scopeStack[-2:0:-1]: importStarred = importStarred or scope.importStarred if not isinstance(scope, FunctionScope): continue try: - scope[name].used = (self.scope, node.lineno) + scope[name].used = (self.scope, node) except KeyError: pass else: @@ -414,14 +453,22 @@ def handleNodeLoad(self, node): # try global scope importStarred = importStarred or self.scopeStack[0].importStarred try: - self.scopeStack[0][name].used = (self.scope, node.lineno) + self.scopeStack[0][name].used = (self.scope, node) except KeyError: - if not importStarred and name not in self.builtIns: - if (os.path.basename(self.filename) == '__init__.py' and name == '__path__'): - # the special name __path__ is valid only in packages - pass - else: - self.report(messages.UndefinedName, node.lineno, name) + pass + else: + return + + # look in the built-ins + if importStarred or name in self.builtIns: + return + if name == '__path__' and os.path.basename(self.filename) == '__init__.py': + # the special name __path__ is valid only in packages + return + + # protected with a NameError handler? + if 'NameError' not in self.exceptHandlers[-1]: + self.report(messages.UndefinedName, node, name) def handleNodeStore(self, node): name = getNodeName(node) @@ -436,17 +483,18 @@ def handleNodeStore(self, node): # 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 (name in scope and scope[name].used and scope[name].used[0] is self.scope - and name not in self.scope.globals): + used = name in scope and scope[name].used + if used and used[0] is self.scope and name not in self.scope.globals: # then it's probably a mistake self.report(messages.UndefinedLocal, - scope[name].used[1], name, scope[name].source.lineno) + scope[name].used[1], name, scope[name].source) break parent = getattr(node, 'parent', None) if isinstance(parent, (ast.For, ast.comprehension, ast.Tuple, ast.List)): binding = Binding(name, node) - elif parent is not None and name == '__all__' and isinstance(self.scope, ModuleScope): + elif (parent is not None and name == '__all__' and + isinstance(self.scope, ModuleScope)): binding = ExportBinding(name, parent.value) else: binding = Assignment(name, node) @@ -459,9 +507,12 @@ def handleNodeDelete(self, node): if not name: return if isinstance(self.scope, FunctionScope) and name in self.scope.globals: - del self.scope.globals[name] + self.scope.globals.remove(name) else: - self.addBinding(node, UnBinding(name, node)) + try: + del self.scope[name] + except KeyError: + self.report(messages.UndefinedName, node, name) def handleChildren(self, tree): for node in iter_child_nodes(tree): @@ -475,32 +526,72 @@ def isDocstring(self, node): return isinstance(node, ast.Str) or (isinstance(node, ast.Expr) and isinstance(node.value, ast.Str)) + def getDocstring(self, node): + if isinstance(node, ast.Expr): + node = node.value + if not isinstance(node, ast.Str): + return (None, None) + # Computed incorrectly if the docstring has backslash + doctest_lineno = node.lineno - node.s.count('\n') - 1 + return (node.s, doctest_lineno) + def handleNode(self, node, parent): if node is None: return - node.parent = parent + if self.offset and getattr(node, 'lineno', None) is not None: + node.lineno += self.offset[0] + node.col_offset += self.offset[1] 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() + self.nodeDepth += 1 node.level = self.nodeDepth + node.parent = parent try: - handler = getattr(self, nodeType) + handler = self.getNodeHandler(node.__class__) handler(node) finally: self.nodeDepth -= 1 if self.traceTree: print(' ' * self.nodeDepth + 'end ' + node.__class__.__name__) + _getDoctestExamples = doctest.DocTestParser().get_examples + + def handleDoctests(self, node): + try: + docstring, node_lineno = self.getDocstring(node.body[0]) + if not docstring: + return + examples = self._getDoctestExamples(docstring) + except (ValueError, IndexError): + # e.g. line 6 of the docstring for has inconsistent + # leading whitespace: ... + return + node_offset = self.offset or (0, 0) + self.pushFunctionScope() + for example in examples: + try: + tree = compile(example.source, "", "exec", ast.PyCF_ONLY_AST) + except SyntaxError: + e = sys.exc_info()[1] + position = (node_lineno + example.lineno + e.lineno, + example.indent + 4 + e.offset) + self.report(messages.DoctestSyntaxError, node, position) + else: + self.offset = (node_offset[0] + node_lineno + example.lineno, + node_offset[1] + example.indent + 4) + self.handleChildren(tree) + self.offset = node_offset + self.popScope() + def ignore(self, node): pass # "stmt" type nodes RETURN = DELETE = PRINT = WHILE = IF = WITH = WITHITEM = RAISE = \ - TRYEXCEPT = TRYFINALLY = TRY = ASSERT = EXEC = EXPR = handleChildren + TRYFINALLY = ASSERT = EXEC = EXPR = handleChildren CONTINUE = BREAK = PASS = ignore @@ -530,7 +621,7 @@ def GLOBAL(self, node): Keep track of globals declarations. """ if isinstance(self.scope, FunctionScope): - self.scope.globals.update(dict.fromkeys(node.names)) + self.scope.globals.update(node.names) NONLOCAL = GLOBAL @@ -579,7 +670,7 @@ def collectLoopVars(n): # 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) + node, varn, self.scope[varn].source) self.handleChildren(node) @@ -587,12 +678,13 @@ def NAME(self, node): """ Handle occurrence of Name (which can be a load/store/delete access.) """ - if node.id == 'locals' and isinstance(node.parent, ast.Call): - # we are doing locals() call in current scope - self.scope.usesLocals = True # Locate the name in locals / function / globals scopes. if isinstance(node.ctx, (ast.Load, ast.AugLoad)): self.handleNodeLoad(node) + if (node.id == 'locals' and isinstance(self.scope, FunctionScope) + and isinstance(node.parent, ast.Call)): + # we are doing locals() call in current scope + self.scope.usesLocals = True elif isinstance(node.ctx, (ast.Store, ast.AugStore)): self.handleNodeStore(node) elif isinstance(node.ctx, ast.Del): @@ -607,6 +699,8 @@ def FUNCTIONDEF(self, node): self.handleNode(deco, node) self.addBinding(node, FunctionDefinition(node.name, node)) self.LAMBDA(node) + if self.withDoctest: + self.deferFunction(lambda: self.handleDoctests(node)) def LAMBDA(self, node): args = [] @@ -619,7 +713,7 @@ def addArgs(arglist): else: if arg.id in args: self.report(messages.DuplicateArgument, - node.lineno, arg.id) + node, arg.id) args.append(arg.id) addArgs(node.args.args) defaults = node.args.defaults @@ -627,7 +721,7 @@ def addArgs(arglist): for arg in node.args.args + node.args.kwonlyargs: if arg.arg in args: self.report(messages.DuplicateArgument, - node.lineno, arg.arg) + node, arg.arg) args.append(arg.arg) self.handleNode(arg.annotation, node) if hasattr(node, 'returns'): # Only for FunctionDefs @@ -641,7 +735,7 @@ def addArgs(arglist): if not wildcard: continue if wildcard in args: - self.report(messages.DuplicateArgument, node.lineno, wildcard) + self.report(messages.DuplicateArgument, node, wildcard) args.append(wildcard) for default in defaults: self.handleNode(default, node) @@ -663,12 +757,8 @@ def checkUnusedAssignments(): """ Check to see if any assignments have not been used. """ - for name, binding in self.scope.items(): - if (not binding.used and name not in self.scope.globals - and not self.scope.usesLocals - and isinstance(binding, Assignment)): - self.report(messages.UnusedVariable, - binding.source.lineno, name) + for name, binding in self.scope.unusedAssignments(): + self.report(messages.UnusedVariable, binding.source, name) self.deferAssignment(checkUnusedAssignments) self.popScope() @@ -688,6 +778,8 @@ def CLASSDEF(self, node): for keywordNode in node.keywords: self.handleNode(keywordNode, node) self.pushClassScope() + if self.withDoctest: + self.deferFunction(lambda: self.handleDoctests(node)) for stmt in node.body: self.handleNode(stmt, node) self.popScope() @@ -713,21 +805,42 @@ 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]) + node, [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) + self.report(messages.ImportStarUsed, node, node.module) continue name = alias.asname or alias.name importation = Importation(name, node) if node.module == '__future__': - importation.used = (self.scope, node.lineno) + importation.used = (self.scope, node) self.addBinding(node, importation) + def TRY(self, node): + handler_names = [] + # List the exception handlers + for handler in node.handlers: + if isinstance(handler.type, ast.Tuple): + for exc_type in handler.type.elts: + handler_names.append(getNodeName(exc_type)) + elif handler.type: + handler_names.append(getNodeName(handler.type)) + # Memorize the except handlers and process the body + self.exceptHandlers.append(handler_names) + for child in node.body: + self.handleNode(child, node) + self.exceptHandlers.pop() + # Process the other nodes: "except:", "else:", "finally:" + for child in iter_child_nodes(node): + if child not in node.body: + self.handleNode(child, node) + + TRYEXCEPT = TRY + def EXCEPTHANDLER(self, node): # 3.x: in addition to handling children, we must handle the name of # the exception, which is not a Name node, but a simple string. diff --git a/pylibs/pylama/pyflakes/messages.py b/pylibs/pylama/pyflakes/messages.py index b15fad63..a4c31985 100644 --- a/pylibs/pylama/pyflakes/messages.py +++ b/pylibs/pylama/pyflakes/messages.py @@ -1,103 +1,118 @@ -# (c) 2005 Divmod, Inc. See LICENSE file for details +""" +Provide the class Message and its subclasses. +""" class Message(object): message = '' message_args = () - def __init__(self, filename, lineno): + def __init__(self, filename, loc): self.filename = filename - self.lineno = lineno + self.lineno = loc.lineno + self.col = getattr(loc, 'col_offset', 0) def __str__(self): - return '%s:%s: %s' % (self.filename, self.lineno, self.message % self.message_args) + return '%s:%s: %s' % (self.filename, self.lineno, + self.message % self.message_args) class UnusedImport(Message): - message = 'W402 %r imported but unused' + message = 'W0611 %r imported but unused' - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc) self.message_args = (name,) class RedefinedWhileUnused(Message): - message = 'W801 redefinition of unused %r from line %r' + message = 'W0404 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) + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc) + self.message_args = (name, orig_loc.lineno) class RedefinedInListComp(Message): - message = 'W801 rlist comprehension redefines %r from line %r' + message = 'W0621 list comprehension redefines %r from line %r' - def __init__(self, filename, lineno, name, orig_lineno): - Message.__init__(self, filename, lineno) - self.message_args = (name, orig_lineno) + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc) + self.message_args = (name, orig_loc.lineno) class ImportShadowedByLoopVar(Message): - message = 'W403 iimport %r from line %r shadowed by loop variable' + message = 'W0621 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) + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc) + self.message_args = (name, orig_loc.lineno) class ImportStarUsed(Message): - message = "W404 ''from %s import *' used; unable to detect undefined names" + message = "W0401 'from %s import *' used; unable to detect undefined names" - def __init__(self, filename, lineno, modname): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, modname): + Message.__init__(self, filename, loc) self.message_args = (modname,) class UndefinedName(Message): - message = 'W802 undefined name %r' + message = 'E0602 undefined name %r' - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc) self.message_args = (name,) +class DoctestSyntaxError(Message): + message = 'W0511 syntax error in doctest' + + def __init__(self, filename, loc, position=None): + Message.__init__(self, filename, loc) + if position: + (self.lineno, self.col) = position + self.message_args = () + + class UndefinedExport(Message): - message = 'W803 undefined name %r in __all__' + message = 'E0603 undefined name %r in __all__' - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc) self.message_args = (name,) class UndefinedLocal(Message): - message = "W804 local variable %r (defined in enclosing scope on line %r) referenced before assignment" + message = ('E0602 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) + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc) + self.message_args = (name, orig_loc.lineno) class DuplicateArgument(Message): - message = 'W805 duplicate argument %r in function definition' + message = 'E1122 duplicate argument %r in function definition' - def __init__(self, filename, lineno, name): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, name): + Message.__init__(self, filename, loc) self.message_args = (name,) class Redefined(Message): - message = 'W806 redefinition of %r from line %r' + message = 'W0621 redefinition of %r from line %r' - def __init__(self, filename, lineno, name, orig_lineno): - Message.__init__(self, filename, lineno) - self.message_args = (name, orig_lineno) + def __init__(self, filename, loc, name, orig_loc): + Message.__init__(self, filename, loc) + self.message_args = (name, orig_loc.lineno) class LateFutureImport(Message): - message = 'W405 future import(s) %r after other statements' + message = 'W0410 future import(s) %r after other statements' - def __init__(self, filename, lineno, names): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, names): + Message.__init__(self, filename, loc) self.message_args = (names,) @@ -106,8 +121,8 @@ 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' + message = 'W0612 local variable %r is assigned to but never used' - def __init__(self, filename, lineno, names): - Message.__init__(self, filename, lineno) + def __init__(self, filename, loc, names): + Message.__init__(self, filename, loc) self.message_args = (names,) diff --git a/pylibs/pylama/pylint/__pkginfo__.py b/pylibs/pylama/pylint/__pkginfo__.py index 5604692c..a018f934 100644 --- a/pylibs/pylama/pylint/__pkginfo__.py +++ b/pylibs/pylama/pylint/__pkginfo__.py @@ -18,16 +18,14 @@ modname = distname = 'pylint' -numversion = (0, 27, 0) +numversion = (0, 28, 0) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common >= 0.53.0', 'logilab-astng >= 0.21.1'] +install_requires = ['logilab-common >= 0.53.0', 'logilab-astng >= 0.24.3'] license = 'GPL' -copyright = 'Logilab S.A.' description = "python code static checker" -web = "http://www.logilab.org/project/%s" % distname -ftp = "ftp://ftp.logilab.org/pub/%s" % modname +web = 'http://www.pylint.org' mailinglist = "mailto://python-projects@lists.logilab.org" author = 'Logilab' author_email = 'python-projects@lists.logilab.org' diff --git a/pylibs/pylama/pylint/checkers/__init__.py b/pylibs/pylama/pylint/checkers/__init__.py index 6c98c9eb..bc722279 100644 --- a/pylibs/pylama/pylint/checkers/__init__.py +++ b/pylibs/pylama/pylint/checkers/__init__.py @@ -30,7 +30,7 @@ 12: logging 13: string_format 14: string_constant -14-50: not yet used: reserved for future internal checkers. +15-50: not yet used: reserved for future internal checkers. 51-99: perhaps used: reserved for external checkers The raw_metrics checker has no number associated since it doesn't emit any diff --git a/pylibs/pylama/pylint/checkers/base.py b/pylibs/pylama/pylint/checkers/base.py index e85dba5a..495076f8 100644 --- a/pylibs/pylama/pylint/checkers/base.py +++ b/pylibs/pylama/pylint/checkers/base.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # Copyright (c) 2009-2010 Arista Networks, Inc. # http://www.logilab.fr/ -- mailto:contact@logilab.fr # This program is free software; you can redistribute it and/or modify it under @@ -66,6 +66,19 @@ def in_nested_list(nested_list, obj): return True return False +def _loop_exits_early(loop): + """Returns true if a loop has a break statement in its body.""" + loop_nodes = (astng.For, astng.While) + # Loop over body explicitly to avoid matching break statements + # in orelse. + for child in loop.body: + if isinstance(child, loop_nodes): + continue + for _ in child.nodes_of_class(astng.Break, skip_klass=loop_nodes): + return True + return False + + def report_by_type_stats(sect, stats, old_stats): """make a report of @@ -160,6 +173,15 @@ class BasicErrorChecker(_BasicChecker): 'nonexistent-operator', "Used when you attempt to use the C-style pre-increment or" "pre-decrement operator -- and ++, which doesn't exist in Python."), + 'E0108': ('Duplicate argument name %s in function definition', + 'duplicate-argument-name', + 'Duplicate argument names in function definitions are syntax' + ' errors.'), + 'W0120': ('Else clause on loop without a break statement', + 'useless-else-on-loop', + 'Loops should only have an else clause if they can exit early ' + 'with a break statement, otherwise the statements under else ' + 'should be on the same scope as the loop itself.'), } def __init__(self, linter): @@ -169,7 +191,7 @@ def __init__(self, linter): def visit_class(self, node): self._check_redefinition('class', node) - @check_messages('E0100', 'E0101', 'E0102', 'E0106') + @check_messages('E0100', 'E0101', 'E0102', 'E0106', 'E0108') def visit_function(self, node): if not redefined_by_decorator(node): self._check_redefinition(node.is_method() and 'method' or 'function', node) @@ -192,6 +214,13 @@ def visit_function(self, node): retnode.value.value is not None: self.add_message('E0106', node=node, line=retnode.fromlineno) + args = set() + for name in node.argnames(): + if name in args: + self.add_message('E0108', node=node, args=(name,)) + else: + args.add(name) + @check_messages('E0104') def visit_return(self, node): @@ -200,7 +229,7 @@ def visit_return(self, node): @check_messages('E0105') def visit_yield(self, node): - if not isinstance(node.frame(), astng.Function): + if not isinstance(node.frame(), (astng.Function, astng.Lambda)): self.add_message('E0105', node=node) @check_messages('E0103') @@ -211,6 +240,14 @@ def visit_continue(self, node): def visit_break(self, node): self._check_in_loop(node, 'break') + @check_messages('W0120') + def visit_for(self, node): + self._check_else_on_loop(node) + + @check_messages('W0120') + def visit_while(self, node): + self._check_else_on_loop(node) + @check_messages('E0107') def visit_unaryop(self, node): """check use of the non-existent ++ adn -- operator operator""" @@ -219,6 +256,15 @@ def visit_unaryop(self, node): (node.operand.op == node.op)): self.add_message('E0107', node=node, args=node.op*2) + def _check_else_on_loop(self, node): + """Check that any loop with an else clause has a break statement.""" + if node.orelse and not _loop_exits_early(node): + self.add_message('W0120', node=node, + # This is not optimal, but the line previous + # to the first statement in the else clause + # will usually be the one that contains the else:. + line=node.orelse[0].lineno - 1) + def _check_in_loop(self, node, node_name): """check that a node is inside a for or while loop""" _node = node.parent @@ -448,18 +494,15 @@ def visit_function(self, node): value = default.infer().next() except astng.InferenceError: continue - if isinstance(value, (astng.Dict, astng.List)): + if (isinstance(value, astng.Instance) and + value.qname() in ('__builtin__.set', '__builtin__.dict', '__builtin__.list')): if value is default: msg = default.as_string() + elif type(value) is astng.Instance: + msg = '%s (%s)' % (default.as_string(), value.qname()) else: msg = '%s (%s)' % (default.as_string(), value.as_string()) self.add_message('W0102', node=node, args=(msg,)) - if value.qname() == '__builtin__.set': - if isinstance(default, astng.CallFunc): - msg = default.as_string() - else: - msg = '%s (%s)' % (default.as_string(), value.qname()) - self.add_message('W0102', node=node, args=(msg,)) @check_messages('W0101', 'W0150') def visit_return(self, node): @@ -534,7 +577,7 @@ def visit_assert(self, node): """check the use of an assert statement on a tuple.""" if node.fail is None and isinstance(node.test, astng.Tuple) and \ len(node.test.elts) == 2: - self.add_message('W0199', line=node.fromlineno, node=node) + self.add_message('W0199', node=node) @check_messages('W0109') def visit_dict(self, node): diff --git a/pylibs/pylama/pylint/checkers/format.py b/pylibs/pylama/pylint/checkers/format.py index 8e165cb8..83da2613 100644 --- a/pylibs/pylama/pylint/checkers/format.py +++ b/pylibs/pylama/pylint/checkers/format.py @@ -1,6 +1,4 @@ -# Copyright (c) 2003-2010 Sylvain Thenault (thenault@gmail.com). -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). -# Copyright 2012 Google Inc. +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -34,6 +32,7 @@ from ..interfaces import IRawChecker, IASTNGChecker from ..checkers import BaseRawChecker from ..checkers.utils import check_messages +from ..utils import WarningScope MSGS = { 'C0301': ('Line too long (%s/%s)', @@ -57,18 +56,22 @@ isn\'t necessary (that\'s python, not C ;).'), 'C0321': ('More than one statement on a single line', 'multiple-statements', - 'Used when more than on statement are found on the same line.'), + 'Used when more than on statement are found on the same line.', + {'scope': WarningScope.NODE}), 'C0322': ('Operator not preceded by a space\n%s', 'no-space-before-operator', 'Used when one of the following operator (!= | <= | == | >= | < ' - '| > | = | \\+= | -= | \\*= | /= | %) is not preceded by a space.'), + '| > | = | \\+= | -= | \\*= | /= | %) is not preceded by a space.', + {'scope': WarningScope.NODE}), 'C0323': ('Operator not followed by a space\n%s', 'no-space-after-operator', 'Used when one of the following operator (!= | <= | == | >= | < ' - '| > | = | \\+= | -= | \\*= | /= | %) is not followed by a space.'), + '| > | = | \\+= | -= | \\*= | /= | %) is not followed by a space.', + {'scope': WarningScope.NODE}), 'C0324': ('Comma not followed by a space\n%s', 'no-space-after-comma', - 'Used when a comma (",") is not followed by a space.'), + 'Used when a comma (",") is not followed by a space.', + {'scope': WarningScope.NODE}), } if sys.version_info < (3, 0): @@ -86,7 +89,8 @@ 'W0333': ('Use of the `` operator', 'backtick', 'Used when the deprecated "``" (backtick) operator is used ' - 'instead of the str() function.'), + 'instead of the str() function.', + {'scope': WarningScope.NODE}), }) # simple quoted string rgx @@ -128,7 +132,6 @@ 'C0324'), ) -_PY3K = sys.version_info >= (3, 0) def get_string_coords(line): """return a list of string positions (tuple (start, end)) in the line @@ -371,106 +374,6 @@ def check_indent_level(self, string, expected, line_num): expected * unit_size)) -class StringConstantChecker(BaseRawChecker): - """Check string literals""" - - msgs = { - 'W1401': ('Anomalous backslash in string: \'%s\'. ' - 'String constant might be missing an r prefix.', - 'anomalous-backslash-in-string', - 'Used when a backslash is in a literal string but not as an ' - 'escape.'), - 'W1402': ('Anomalous Unicode escape in byte string: \'%s\'. ' - 'String constant might be missing an r or u prefix.', - 'anomalous-unicode-escape-in-string', - 'Used when an escape like \\u is encountered in a byte ' - 'string where it has no effect.'), - } - name = 'string_constant' - __implements__ = (IRawChecker, IASTNGChecker) - - # Characters that have a special meaning after a backslash in either - # Unicode or byte strings. - ESCAPE_CHARACTERS = 'abfnrtvx\n\r\t\\\'\"01234567' - - # TODO(mbp): Octal characters are quite an edge case today; people may - # prefer a separate warning where they occur. \0 should be allowed. - - # Characters that have a special meaning after a backslash but only in - # Unicode strings. - UNICODE_ESCAPE_CHARACTERS = 'uUN' - - def process_tokens(self, tokens): - for (tok_type, token, (start_row, start_col), _, _) in tokens: - if tok_type == tokenize.STRING: - # 'token' is the whole un-parsed token; we can look at the start - # of it to see whether it's a raw or unicode string etc. - self.process_string_token(token, start_row, start_col) - - def process_string_token(self, token, start_row, start_col): - for i, c in enumerate(token): - if c in '\'\"': - quote_char = c - break - prefix = token[:i].lower() # markers like u, b, r. - after_prefix = token[i:] - if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char: - string_body = after_prefix[3:-3] - else: - string_body = after_prefix[1:-1] # Chop off quotes - # No special checks on raw strings at the moment. - if 'r' not in prefix: - self.process_non_raw_string_token(prefix, string_body, - start_row, start_col) - - def process_non_raw_string_token(self, prefix, string_body, start_row, - start_col): - """check for bad escapes in a non-raw string. - - prefix: lowercase string of eg 'ur' string prefix markers. - string_body: the un-parsed body of the string, not including the quote - marks. - start_row: integer line number in the source. - start_col: integer column number in the source. - """ - # Walk through the string; if we see a backslash then escape the next - # character, and skip over it. If we see a non-escaped character, - # alert, and continue. - # - # Accept a backslash when it escapes a backslash, or a quote, or - # end-of-line, or one of the letters that introduce a special escape - # sequence - # - # TODO(mbp): Maybe give a separate warning about the rarely-used - # \a \b \v \f? - # - # TODO(mbp): We could give the column of the problem character, but - # add_message doesn't seem to have a way to pass it through at present. - i = 0 - while True: - i = string_body.find('\\', i) - if i == -1: - break - # There must be a next character; having a backslash at the end - # of the string would be a SyntaxError. - next_char = string_body[i+1] - match = string_body[i:i+2] - if next_char in self.UNICODE_ESCAPE_CHARACTERS: - if 'u' in prefix: - pass - elif _PY3K and 'b' not in prefix: - pass # unicode by default - else: - self.add_message('W1402', line=start_row, args=(match, )) - elif next_char not in self.ESCAPE_CHARACTERS: - self.add_message('W1401', line=start_row, args=(match, )) - # Whether it was a valid escape or not, backslash followed by - # another character can always be consumed whole: the second - # character can never be the start of a new backslash escape. - i += 2 - - def register(linter): """required method to auto register this checker """ linter.register_checker(FormatChecker(linter)) - linter.register_checker(StringConstantChecker(linter)) diff --git a/pylibs/pylama/pylint/checkers/imports.py b/pylibs/pylama/pylint/checkers/imports.py index b94e3685..e243ff6b 100644 --- a/pylibs/pylama/pylint/checkers/imports.py +++ b/pylibs/pylama/pylint/checkers/imports.py @@ -52,21 +52,6 @@ def get_first_import(node, context, name, base, level): # utilities to represents import dependencies as tree and dot graph ########### -def filter_dependencies_info(dep_info, package_dir, mode='external'): - """filter external or internal dependencies from dep_info (return a - new dictionary containing the filtered modules only) - """ - if mode == 'external': - filter_func = lambda x: not is_standard_module(x, (package_dir,)) - else: - assert mode == 'internal' - filter_func = lambda x: is_standard_module(x, (package_dir,)) - result = {} - for importee, importers in dep_info.iteritems(): - if filter_func(importee): - result[importee] = importers - return result - def make_tree_defs(mod_files_list): """get a list of 2-uple (module, list_of_files_which_import_this_module), it will return a dictionary to represent this as a tree @@ -313,7 +298,7 @@ def _add_imported_module(self, node, importedmodname): importedmodname, set()) if not context_name in importedmodnames: importedmodnames.add(context_name) - if is_standard_module( importedmodname, (self.package_dir(),) ): + if is_standard_module(importedmodname, (self.package_dir(),)): # update import graph mgraph = self.import_graph.setdefault(context_name, set()) if not importedmodname in mgraph: @@ -373,8 +358,11 @@ def _external_dependencies_info(self): cache them """ if self.__ext_dep_info is None: - self.__ext_dep_info = filter_dependencies_info( - self.stats['dependencies'], self.package_dir(), 'external') + package = self.linter.base_name + self.__ext_dep_info = result = {} + for importee, importers in self.stats['dependencies'].iteritems(): + if not importee.startswith(package): + result[importee] = importers return self.__ext_dep_info def _internal_dependencies_info(self): @@ -382,8 +370,11 @@ def _internal_dependencies_info(self): cache them """ if self.__int_dep_info is None: - self.__int_dep_info = filter_dependencies_info( - self.stats['dependencies'], self.package_dir(), 'internal') + package = self.linter.base_name + self.__int_dep_info = result = {} + for importee, importers in self.stats['dependencies'].iteritems(): + if importee.startswith(package): + result[importee] = importers return self.__int_dep_info diff --git a/pylibs/pylama/pylint/checkers/logging.py b/pylibs/pylama/pylint/checkers/logging.py index 4c78d8f2..bb95a539 100644 --- a/pylibs/pylama/pylint/checkers/logging.py +++ b/pylibs/pylama/pylint/checkers/logging.py @@ -85,9 +85,9 @@ def visit_callfunc(self, node): try: logger_class = [inferred for inferred in node.func.expr.infer() if ( isinstance(inferred, astng.Instance) - and [ancestor for ancestor in inferred._proxied.ancestors() if ( - ancestor.name == 'Logger' - and ancestor.parent.name == 'logging')])] + and any(ancestor for ancestor in inferred._proxied.ancestors() if ( + ancestor.name == 'Logger' + and ancestor.parent.name == 'logging')))] except astng.exceptions.InferenceError: return if (node.func.expr.name != self._logging_name and not logger_class): diff --git a/pylibs/pylama/pylint/checkers/string_format.py b/pylibs/pylama/pylint/checkers/strings.py similarity index 56% rename from pylibs/pylama/pylint/checkers/string_format.py rename to pylibs/pylama/pylint/checkers/strings.py index fdec7022..3c755dd7 100644 --- a/pylibs/pylama/pylint/checkers/string_format.py +++ b/pylibs/pylama/pylint/checkers/strings.py @@ -1,5 +1,7 @@ # Copyright (c) 2009-2010 Arista Networks, Inc. - James Lingard -# Copyright (c) 2004-2010 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE). +# Copyright 2012 Google Inc. +# # http://www.logilab.fr/ -- mailto:contact@logilab.fr # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -16,11 +18,16 @@ """Checker for string formatting operations. """ +import sys +import tokenize + from ..logilab import astng -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker + +from ..interfaces import IRawChecker, IASTNGChecker +from ..checkers import BaseChecker, BaseRawChecker from ..checkers import utils +_PY3K = sys.version_info >= (3, 0) MSGS = { 'E1300': ("Unsupported format character %r (%#02x) at index %d", @@ -75,7 +82,7 @@ class StringFormatChecker(BaseChecker): """ __implements__ = (IASTNGChecker,) - name = 'string_format' + name = 'string' msgs = MSGS def visit_binop(self, node): @@ -158,6 +165,132 @@ def visit_binop(self, node): self.add_message('E1306', node=node) +class StringMethodsChecker(BaseChecker): + __implements__ = (IASTNGChecker,) + name = 'string' + msgs = { + 'E1310': ("Suspicious argument in %s.%s call", + "bad-str-strip-call", + "The argument to a str.{l,r,}strip call contains a" + " duplicate character, "), + } + + def visit_callfunc(self, node): + func = utils.safe_infer(node.func) + if (isinstance(func, astng.BoundMethod) + and isinstance(func.bound, astng.Instance) + and func.bound.name in ('str', 'unicode', 'bytes') + and func.name in ('strip', 'lstrip', 'rstrip') + and node.args): + arg = utils.safe_infer(node.args[0]) + if not isinstance(arg, astng.Const): + return + if len(arg.value) != len(set(arg.value)): + self.add_message('E1310', node=node, + args=(func.bound.name, func.name)) + + +class StringConstantChecker(BaseRawChecker): + """Check string literals""" + __implements__ = (IRawChecker, IASTNGChecker) + name = 'string_constant' + msgs = { + 'W1401': ('Anomalous backslash in string: \'%s\'. ' + 'String constant might be missing an r prefix.', + 'anomalous-backslash-in-string', + 'Used when a backslash is in a literal string but not as an ' + 'escape.'), + 'W1402': ('Anomalous Unicode escape in byte string: \'%s\'. ' + 'String constant might be missing an r or u prefix.', + 'anomalous-unicode-escape-in-string', + 'Used when an escape like \\u is encountered in a byte ' + 'string where it has no effect.'), + } + + # Characters that have a special meaning after a backslash in either + # Unicode or byte strings. + ESCAPE_CHARACTERS = 'abfnrtvx\n\r\t\\\'\"01234567' + + # TODO(mbp): Octal characters are quite an edge case today; people may + # prefer a separate warning where they occur. \0 should be allowed. + + # Characters that have a special meaning after a backslash but only in + # Unicode strings. + UNICODE_ESCAPE_CHARACTERS = 'uUN' + + def process_tokens(self, tokens): + for (tok_type, token, (start_row, start_col), _, _) in tokens: + if tok_type == tokenize.STRING: + # 'token' is the whole un-parsed token; we can look at the start + # of it to see whether it's a raw or unicode string etc. + self.process_string_token(token, start_row, start_col) + + def process_string_token(self, token, start_row, start_col): + for i, c in enumerate(token): + if c in '\'\"': + quote_char = c + break + prefix = token[:i].lower() # markers like u, b, r. + after_prefix = token[i:] + if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char: + string_body = after_prefix[3:-3] + else: + string_body = after_prefix[1:-1] # Chop off quotes + # No special checks on raw strings at the moment. + if 'r' not in prefix: + self.process_non_raw_string_token(prefix, string_body, + start_row, start_col) + + def process_non_raw_string_token(self, prefix, string_body, start_row, + start_col): + """check for bad escapes in a non-raw string. + + prefix: lowercase string of eg 'ur' string prefix markers. + string_body: the un-parsed body of the string, not including the quote + marks. + start_row: integer line number in the source. + start_col: integer column number in the source. + """ + # Walk through the string; if we see a backslash then escape the next + # character, and skip over it. If we see a non-escaped character, + # alert, and continue. + # + # Accept a backslash when it escapes a backslash, or a quote, or + # end-of-line, or one of the letters that introduce a special escape + # sequence + # + # TODO(mbp): Maybe give a separate warning about the rarely-used + # \a \b \v \f? + # + # TODO(mbp): We could give the column of the problem character, but + # add_message doesn't seem to have a way to pass it through at present. + i = 0 + while True: + i = string_body.find('\\', i) + if i == -1: + break + # There must be a next character; having a backslash at the end + # of the string would be a SyntaxError. + next_char = string_body[i+1] + match = string_body[i:i+2] + if next_char in self.UNICODE_ESCAPE_CHARACTERS: + if 'u' in prefix: + pass + elif _PY3K and 'b' not in prefix: + pass # unicode by default + else: + self.add_message('W1402', line=start_row, args=(match, )) + elif next_char not in self.ESCAPE_CHARACTERS: + self.add_message('W1401', line=start_row, args=(match, )) + # Whether it was a valid escape or not, backslash followed by + # another character can always be consumed whole: the second + # character can never be the start of a new backslash escape. + i += 2 + + + def register(linter): """required method to auto register this checker """ linter.register_checker(StringFormatChecker(linter)) + linter.register_checker(StringMethodsChecker(linter)) + linter.register_checker(StringConstantChecker(linter)) diff --git a/pylibs/pylama/pylint/checkers/typecheck.py b/pylibs/pylama/pylint/checkers/typecheck.py index c6309aea..4cdc6006 100644 --- a/pylibs/pylama/pylint/checkers/typecheck.py +++ b/pylibs/pylama/pylint/checkers/typecheck.py @@ -63,7 +63,7 @@ 'unexpected-keyword-arg', 'Used when a function call passes a keyword argument that \ doesn\'t correspond to one of the function\'s parameter names.'), - 'E1124': ('Multiple values passed for parameter %r in function call', + 'E1124': ('Parameter %r passed as both positional and keyword argument', 'redundant-keyword-arg', 'Used when a function call would result in assigning multiple \ values to a function parameter, one value from a positional \ diff --git a/pylibs/pylama/pylint/checkers/utils.py b/pylibs/pylama/pylint/checkers/utils.py index 193f6cd9..34d335bb 100644 --- a/pylibs/pylama/pylint/checkers/utils.py +++ b/pylibs/pylama/pylint/checkers/utils.py @@ -20,9 +20,11 @@ import re import string + from ..logilab import astng from ..logilab.astng import scoped_nodes from ..logilab.common.compat import builtins + BUILTINS_NAME = builtins.__name__ COMP_NODE_TYPES = astng.ListComp, astng.SetComp, astng.DictComp, astng.GenExpr @@ -365,6 +367,7 @@ def is_super_call(expr): return (isinstance(expr, astng.CallFunc) and isinstance(expr.func, astng.Name) and expr.func.name == 'super') + def is_attr_private(attrname): """Check that attribute name is private (at least two leading underscores, at most one trailing underscore) diff --git a/pylibs/pylama/pylint/lint.py b/pylibs/pylama/pylint/lint.py index e1cc8b6f..60a9784b 100644 --- a/pylibs/pylama/pylint/lint.py +++ b/pylibs/pylama/pylint/lint.py @@ -1,5 +1,4 @@ -# Copyright (c) 2003-2010 Sylvain Thenault (thenault@gmail.com). -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -48,7 +47,8 @@ from .logilab.astng.__pkginfo__ import version as astng_version from .utils import (PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, - ReportsHandlerMixIn, MSG_TYPES, expand_modules) + ReportsHandlerMixIn, MSG_TYPES, expand_modules, + WarningScope) from .interfaces import ILinter, IRawChecker, IASTNGChecker from .checkers import (BaseRawChecker, EmptyReport, table_lines_from_stats) @@ -508,11 +508,16 @@ def collect_block_lines(self, node, msg_state): for lineno, state in lines.items(): original_lineno = lineno if first <= lineno <= last: - if lineno > firstchildlineno: - state = True - # set state for all lines for this block - first, last = node.block_range(lineno) - for line in xrange(first, last+1): + # Set state for all lines for this block, if the + # warning is applied to nodes. + if self._messages[msgid].scope == WarningScope.NODE: + if lineno > firstchildlineno: + state = True + first_, last_ = node.block_range(lineno) + else: + first_ = lineno + last_ = last + for line in xrange(first_, last_+1): # do not override existing entries if not line in self._module_msgs_state.get(msgid, ()): if line in lines: # state change in the same block @@ -624,7 +629,6 @@ def set_current_module(self, modname, filepath=None): # messages which are only detected in the .close step) if modname: self._module_msgs_state = {} - self._module_msg_cats_state = {} self._raw_module_msgs_state = {} self._ignored_msgs = {} @@ -707,8 +711,8 @@ def _add_suppression_messages(self): if not enable and (warning, line) not in self._ignored_msgs: self.add_message('I0021', line, None, (self.get_msg_display_string(warning),)) - - for (warning, from_), lines in self._ignored_msgs.iteritems(): + # don't use iteritems here, _ignored_msgs may be modified by add_message + for (warning, from_), lines in self._ignored_msgs.items(): for line in lines: self.add_message('I0020', line, None, (self.get_msg_display_string(warning), from_)) @@ -856,8 +860,8 @@ def __init__(self, args, reporter=None, exit=True): 'rcfile': (self.cb_set_rcfile, True), 'load-plugins': (self.cb_add_plugins, True), }) - except ArgumentPreprocessingError, e: - print >> sys.stderr, 'Argument %s expects a value.' % (e.args[0],) + except ArgumentPreprocessingError, ex: + print >> sys.stderr, 'Argument %s expects a value.' % (ex.args[0],) sys.exit(32) self.linter = linter = self.LinterClass(( diff --git a/pylibs/pylama/pylint/logilab/astng/__init__.py b/pylibs/pylama/pylint/logilab/astng/__init__.py index 6c4a87f1..555838e3 100644 --- a/pylibs/pylama/pylint/logilab/astng/__init__.py +++ b/pylibs/pylama/pylint/logilab/astng/__init__.py @@ -1,7 +1,5 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -44,10 +42,6 @@ __doctype__ = "restructuredtext en" import sys -if sys.version_info >= (3, 0): - BUILTINS_MODULE = 'builtins' -else: - BUILTINS_MODULE = '__builtin__' # WARNING: internal imports order matters ! diff --git a/pylibs/pylama/pylint/logilab/astng/__pkginfo__.py b/pylibs/pylama/pylint/logilab/astng/__pkginfo__.py index f5097f48..31de45d0 100644 --- a/pylibs/pylama/pylint/logilab/astng/__pkginfo__.py +++ b/pylibs/pylama/pylint/logilab/astng/__pkginfo__.py @@ -1,7 +1,5 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -24,7 +22,7 @@ modname = 'astng' subpackage_of = 'logilab' -numversion = (0, 24, 2) +numversion = (0, 24, 3) version = '.'.join([str(num) for num in numversion]) install_requires = ['logilab-common >= 0.53.0'] diff --git a/pylibs/pylama/pylint/logilab/astng/as_string.py b/pylibs/pylama/pylint/logilab/astng/as_string.py index 0a42668d..c21144e5 100644 --- a/pylibs/pylama/pylint/logilab/astng/as_string.py +++ b/pylibs/pylama/pylint/logilab/astng/as_string.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -17,29 +15,63 @@ # # You should have received a copy of the GNU Lesser General Public License along # with logilab-astng. If not, see . -"""This module renders ASTNG nodes to string representation. +"""This module renders ASTNG nodes as string: -It will probably not work on bare _ast trees. +* :func:`to_code` function return equivalent (hopefuly valid) python string + +* :func:`dump` function return an internal representation of nodes found + in the tree, useful for debugging or understanding the tree structure """ -import sys +import sys INDENT = ' ' # 4 spaces ; keep indentation variable -def _import_string(names): - """return a list of (name, asname) formatted as a string""" - _names = [] - for name, asname in names: - if asname is not None: - _names.append('%s as %s' % (name, asname)) +def dump(node, ids=False): + """print a nice astng tree representation. + + :param ids: if true, we also print the ids (usefull for debugging) + """ + result = [] + _repr_tree(node, result, ids=ids) + return "\n".join(result) + +def _repr_tree(node, result, indent='', _done=None, ids=False): + """built a tree representation of a node as a list of lines""" + if _done is None: + _done = set() + if not hasattr(node, '_astng_fields'): # not a astng node + return + if node in _done: + result.append( indent + 'loop in tree: %s' % node ) + return + _done.add(node) + node_str = str(node) + if ids: + node_str += ' . \t%x' % id(node) + result.append( indent + node_str ) + indent += INDENT + for field in node._astng_fields: + value = getattr(node, field) + if isinstance(value, (list, tuple) ): + result.append( indent + field + " = [" ) + for child in value: + if isinstance(child, (list, tuple) ): + # special case for Dict # FIXME + _repr_tree(child[0], result, indent, _done, ids) + _repr_tree(child[1], result, indent, _done, ids) + result.append(indent + ',') + else: + _repr_tree(child, result, indent, _done, ids) + result.append( indent + "]" ) else: - _names.append(name) - return ', '.join(_names) + result.append( indent + field + " = " ) + _repr_tree(value, result, indent, _done, ids) class AsStringVisitor(object): - """Visitor to render an ASTNG node as string """ + """Visitor to render an ASTNG node as a valid python code string""" def __call__(self, node): """Makes this visitor behave as a simple function""" @@ -385,7 +417,11 @@ def visit_with(self, node): # 'with' without 'as' is possible def visit_yield(self, node): """yield an ast.Yield node as string""" yi_val = node.value and (" " + node.value.accept(self)) or "" - return 'yield' + yi_val + expr = 'yield' + yi_val + if node.parent.is_statement: + return expr + else: + return "(%s)" % (expr,) class AsStringVisitor3k(AsStringVisitor): @@ -419,9 +455,21 @@ def visit_starred(self, node): """return Starred node as string""" return "*" + node.value.accept(self) + +def _import_string(names): + """return a list of (name, asname) formatted as a string""" + _names = [] + for name, asname in names: + if asname is not None: + _names.append('%s as %s' % (name, asname)) + else: + _names.append(name) + return ', '.join(_names) + + if sys.version_info >= (3, 0): AsStringVisitor = AsStringVisitor3k # this visitor is stateless, thus it can be reused -as_string = AsStringVisitor() +to_code = AsStringVisitor() diff --git a/pylibs/pylama/pylint/logilab/astng/bases.py b/pylibs/pylama/pylint/logilab/astng/bases.py index 42d34438..6331dc8e 100644 --- a/pylibs/pylama/pylint/logilab/astng/bases.py +++ b/pylibs/pylama/pylint/logilab/astng/bases.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -24,20 +21,23 @@ __docformat__ = "restructuredtext en" +import sys from contextlib import contextmanager -from ..common.compat import builtins +from .exceptions import (InferenceError, ASTNGError, + NotFoundError, UnresolvableName) -from . import BUILTINS_MODULE -from .exceptions import InferenceError, ASTNGError, \ - NotFoundError, UnresolvableName -from .as_string import as_string -BUILTINS_NAME = builtins.__name__ +if sys.version_info >= (3, 0): + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' + class Proxy(object): """a simple proxy object""" - _proxied = None + + _proxied = None # proxied object may be set by class or by instance def __init__(self, proxied=None): if proxied is not None: @@ -188,7 +188,7 @@ def _wrap_attr(self, attrs, context=None): """wrap bound methods of attrs in a InstanceMethod proxies""" for attr in attrs: if isinstance(attr, UnboundMethod): - if BUILTINS_NAME + '.property' in attr.decoratornames(): + if BUILTINS + '.property' in attr.decoratornames(): for infered in attr.infer_call_result(self, context): yield infered else: @@ -253,7 +253,7 @@ def infer_call_result(self, caller, context): # If we're unbound method __new__ of builtin object, the result is an # instance of the class given as first argument. if (self._proxied.name == '__new__' and - self._proxied.parent.frame().qname() == '%s.object' % BUILTINS_MODULE): + self._proxied.parent.frame().qname() == '%s.object' % BUILTINS): return (x is YES and x or Instance(x) for x in caller.args[0].infer()) return self._proxied.infer_call_result(caller, context) @@ -274,12 +274,15 @@ def infer_call_result(self, caller, context): class Generator(Instance): - """a special node representing a generator""" + """a special node representing a generator. + + Proxied class is set once for all in raw_building. + """ def callable(self): - return True + return False def pytype(self): - return '%s.generator' % BUILTINS_MODULE + return '%s.generator' % BUILTINS def display_type(self): return 'Generator' @@ -563,15 +566,12 @@ def eq(self, value): return False def as_string(self): - return as_string(self) + from .as_string import to_code + return to_code(self) def repr_tree(self, ids=False): - """print a nice astng tree representation. - - :param ids: if true, we also print the ids (usefull for debugging)""" - result = [] - _repr_tree(self, result, ids=ids) - return "\n".join(result) + from .as_string import dump + return dump(self) class Statement(NodeNG): @@ -593,39 +593,3 @@ def previous_sibling(self): index = stmts.index(self) if index >= 1: return stmts[index -1] - -INDENT = " " - -def _repr_tree(node, result, indent='', _done=None, ids=False): - """built a tree representation of a node as a list of lines""" - if _done is None: - _done = set() - if not hasattr(node, '_astng_fields'): # not a astng node - return - if node in _done: - result.append( indent + 'loop in tree: %s' % node ) - return - _done.add(node) - node_str = str(node) - if ids: - node_str += ' . \t%x' % id(node) - result.append( indent + node_str ) - indent += INDENT - for field in node._astng_fields: - value = getattr(node, field) - if isinstance(value, (list, tuple) ): - result.append( indent + field + " = [" ) - for child in value: - if isinstance(child, (list, tuple) ): - # special case for Dict # FIXME - _repr_tree(child[0], result, indent, _done, ids) - _repr_tree(child[1], result, indent, _done, ids) - result.append(indent + ',') - else: - _repr_tree(child, result, indent, _done, ids) - result.append( indent + "]" ) - else: - result.append( indent + field + " = " ) - _repr_tree(value, result, indent, _done, ids) - - diff --git a/pylibs/pylama/pylint/logilab/astng/brain/py2stdlib.py b/pylibs/pylama/pylint/logilab/astng/brain/py2stdlib.py index fb3ccdc1..d7b19532 100644 --- a/pylibs/pylama/pylint/logilab/astng/brain/py2stdlib.py +++ b/pylibs/pylama/pylint/logilab/astng/brain/py2stdlib.py @@ -15,11 +15,17 @@ def hashlib_transform(module): class md5(object): def __init__(self, value): pass + def digest(): + return u'' + def update(self, value): pass def hexdigest(self): return u'' class sha1(object): def __init__(self, value): pass + def digest(): + return u'' + def update(self, value): pass def hexdigest(self): return u'' @@ -99,7 +105,7 @@ def cleanup_resources(force=False): def urlparse_transform(module): fake = ASTNGBuilder(MANAGER).string_build(''' -def urlparse(urlstring, default_scheme='', allow_fragments=True): +def urlparse(url, scheme='', allow_fragments=True): return ParseResult() class ParseResult(object): diff --git a/pylibs/pylama/pylint/logilab/astng/builder.py b/pylibs/pylama/pylint/logilab/astng/builder.py index 190d5886..ae0c7d01 100644 --- a/pylibs/pylama/pylint/logilab/astng/builder.py +++ b/pylibs/pylama/pylint/logilab/astng/builder.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -25,8 +23,8 @@ __docformat__ = "restructuredtext en" -import sys, re -from os.path import splitext, basename, dirname, exists, abspath +import sys +from os.path import splitext, basename, exists, abspath from ..common.modutils import modpath_from_file @@ -88,6 +86,7 @@ class ASTNGBuilder(InspectBuilder): rebuilder = TreeRebuilder() def __init__(self, manager=None): + InspectBuilder.__init__(self) self._manager = manager or MANAGER def module_build(self, module, modname=None): diff --git a/pylibs/pylama/pylint/logilab/astng/exceptions.py b/pylibs/pylama/pylint/logilab/astng/exceptions.py index 7dd6135e..db33f8b9 100644 --- a/pylibs/pylama/pylint/logilab/astng/exceptions.py +++ b/pylibs/pylama/pylint/logilab/astng/exceptions.py @@ -1,19 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # diff --git a/pylibs/pylama/pylint/logilab/astng/inference.py b/pylibs/pylama/pylint/logilab/astng/inference.py index faf8bfb2..fda25cf8 100644 --- a/pylibs/pylama/pylint/logilab/astng/inference.py +++ b/pylibs/pylama/pylint/logilab/astng/inference.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -23,14 +21,13 @@ __doctype__ = "restructuredtext en" from itertools import chain -import sys from . import nodes from .manager import ASTNGManager -from .exceptions import (ASTNGBuildingException, ASTNGError, +from .exceptions import (ASTNGError, InferenceError, NoDefault, NotFoundError, UnresolvableName) -from .bases import YES, Instance, InferenceContext, Generator, \ +from .bases import YES, Instance, InferenceContext, \ _infer_stmts, copy_context, path_wrapper, raise_if_nothing_infered from .protocols import _arguments_infer_argname @@ -137,7 +134,7 @@ def infer_end(self, context=None): nodes.List.infer = infer_end nodes.Tuple.infer = infer_end nodes.Dict.infer = infer_end - +nodes.Set.infer = infer_end def infer_name(self, context=None): """infer a Name: use name lookup rules""" @@ -238,15 +235,19 @@ def infer_global(self, context=None): def infer_subscript(self, context=None): """infer simple subscription such as [1,2,3][0] or (1,2,3)[-1]""" - if isinstance(self.slice, nodes.Index): - index = self.slice.value.infer(context).next() - if index is YES: - yield YES - return + value = self.value.infer(context).next() + if value is YES: + yield YES + return + + index = self.slice.infer(context).next() + if index is YES: + yield YES + return + + if isinstance(index, nodes.Const): try: - # suppose it's a Tuple/List node (attribute error else) - # XXX infer self.value? - assigned = self.value.getitem(index.value, context) + assigned = value.getitem(index.value, context) except AttributeError: raise InferenceError() except (IndexError, TypeError): @@ -381,3 +382,7 @@ def infer_empty_node(self, context=None): yield YES nodes.EmptyNode.infer = path_wrapper(infer_empty_node) + +def infer_index(self, context=None): + return self.value.infer(context) +nodes.Index.infer = infer_index diff --git a/pylibs/pylama/pylint/logilab/astng/manager.py b/pylibs/pylama/pylint/logilab/astng/manager.py index 513ad708..d090f80b 100644 --- a/pylibs/pylama/pylint/logilab/astng/manager.py +++ b/pylibs/pylama/pylint/logilab/astng/manager.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -24,9 +22,8 @@ __docformat__ = "restructuredtext en" -import sys import os -from os.path import dirname, basename, abspath, join, isdir, exists +from os.path import dirname, join, isdir, exists from ..common.modutils import NoSourceFile, is_python_source, \ file_from_modpath, load_module_from_name, modpath_from_file, \ diff --git a/pylibs/pylama/pylint/logilab/astng/mixins.py b/pylibs/pylama/pylint/logilab/astng/mixins.py index 207dc70b..5d4a865c 100644 --- a/pylibs/pylama/pylint/logilab/astng/mixins.py +++ b/pylibs/pylama/pylint/logilab/astng/mixins.py @@ -1,19 +1,5 @@ -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -55,6 +41,7 @@ def _elsed_block_range(self, lineno, orelse, last=None): return lineno, orelse[0].fromlineno - 1 return lineno, last or self.tolineno + class FilterStmtsMixin(object): """Mixin for statement filtering and assignment type""" @@ -92,7 +79,6 @@ def ass_type(self): return self.parent.ass_type() - class FromImportMixIn(FilterStmtsMixin): """MixIn for From and Import Nodes""" diff --git a/pylibs/pylama/pylint/logilab/astng/node_classes.py b/pylibs/pylama/pylint/logilab/astng/node_classes.py index ef8c5e18..a7976774 100644 --- a/pylibs/pylama/pylint/logilab/astng/node_classes.py +++ b/pylibs/pylama/pylint/logilab/astng/node_classes.py @@ -1,7 +1,5 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -22,10 +20,9 @@ import sys -from . import BUILTINS_MODULE from .exceptions import NoDefault from .bases import (NodeNG, Statement, Instance, InferenceContext, - _infer_stmts, YES) + _infer_stmts, YES, BUILTINS) from .mixins import BlockRangeMixIn, AssignTypeMixin, \ ParentAssignTypeMixin, FromImportMixIn @@ -495,7 +492,7 @@ def __init__(self, items=None): for k,v in items.iteritems()] def pytype(self): - return '%s.dict' % BUILTINS_MODULE + return '%s.dict' % BUILTINS def get_children(self): """get children of a Dict node""" @@ -513,14 +510,16 @@ def last_child(self): def itered(self): return self.items[::2] - def getitem(self, key, context=None): - for i in xrange(0, len(self.items), 2): - for inferedkey in self.items[i].infer(context): + def getitem(self, lookup_key, context=None): + for key, value in self.items: + for inferedkey in key.infer(context): if inferedkey is YES: continue - if isinstance(inferedkey, Const) and inferedkey.value == key: - return self.items[i+1] - raise IndexError(key) + if isinstance(inferedkey, Const) and inferedkey.value == lookup_key: + return value + # This should raise KeyError, but all call sites only catch + # IndexError. Let's leave it like that for now. + raise IndexError(lookup_key) class Discard(Statement): @@ -670,7 +669,7 @@ def __init__(self, elts=None): self.elts = [const_factory(e) for e in elts] def pytype(self): - return '%s.list' % BUILTINS_MODULE + return '%s.list' % BUILTINS def getitem(self, index, context=None): return self.elts[index] @@ -737,7 +736,7 @@ def __init__(self, elts=None): self.elts = [const_factory(e) for e in elts] def pytype(self): - return '%s.set' % BUILTINS_MODULE + return '%s.set' % BUILTINS def itered(self): return self.elts @@ -819,7 +818,7 @@ def __init__(self, elts=None): self.elts = [const_factory(e) for e in elts] def pytype(self): - return '%s.tuple' % BUILTINS_MODULE + return '%s.tuple' % BUILTINS def getitem(self, index, context=None): return self.elts[index] @@ -891,18 +890,15 @@ def _update_const_classes(): def const_factory(value): """return an astng node for a python value""" - # since const_factory is called to evaluate content of container (eg list, - # tuple), it may be called with some node as argument that should be left - # untouched - if isinstance(value, NodeNG): - return value + # XXX we should probably be stricter here and only consider stuff in + # CONST_CLS or do better treatment: in case where value is not in CONST_CLS, + # we should rather recall the builder on this value than returning an empty + # node (another option being that const_factory shouldn't be called with something + # not in CONST_CLS) + assert not isinstance(value, NodeNG) try: return CONST_CLS[value.__class__](value) except (KeyError, AttributeError): - # some constants (like from gtk._gtk) don't have their class in - # CONST_CLS, though we can "assert isinstance(value, tuple(CONST_CLS))" - if isinstance(value, tuple(CONST_CLS)): - return Const(value) node = EmptyNode() node.object = value return node diff --git a/pylibs/pylama/pylint/logilab/astng/nodes.py b/pylibs/pylama/pylint/logilab/astng/nodes.py index 39ab0412..b475b902 100644 --- a/pylibs/pylama/pylint/logilab/astng/nodes.py +++ b/pylibs/pylama/pylint/logilab/astng/nodes.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # diff --git a/pylibs/pylama/pylint/logilab/astng/protocols.py b/pylibs/pylama/pylint/logilab/astng/protocols.py index 00d6be81..05feb246 100644 --- a/pylibs/pylama/pylint/logilab/astng/protocols.py +++ b/pylibs/pylama/pylint/logilab/astng/protocols.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -26,7 +24,7 @@ from .exceptions import InferenceError, NoDefault from .node_classes import unpack_infer from .bases import copy_context, \ - raise_if_nothing_infered, yes_if_nothing_infered, Instance, Generator, YES + raise_if_nothing_infered, yes_if_nothing_infered, Instance, YES from .nodes import const_factory from . import nodes diff --git a/pylibs/pylama/pylint/logilab/astng/raw_building.py b/pylibs/pylama/pylint/logilab/astng/raw_building.py index 5f308cae..076f28f3 100644 --- a/pylibs/pylama/pylint/logilab/astng/raw_building.py +++ b/pylibs/pylama/pylint/logilab/astng/raw_building.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -28,11 +26,10 @@ from inspect import (getargspec, isdatadescriptor, isfunction, ismethod, ismethoddescriptor, isclass, isbuiltin) -from . import BUILTINS_MODULE from .node_classes import CONST_CLS from .nodes import (Module, Class, Const, const_factory, From, - Function, EmptyNode, Name, Arguments, Dict, List, Set, Tuple) -from .bases import Generator + Function, EmptyNode, Name, Arguments) +from .bases import BUILTINS, Generator from .manager import ASTNGManager MANAGER = ASTNGManager() @@ -277,7 +274,7 @@ def object_build(self, node, obj): elif isdatadescriptor(member): assert isinstance(member, object) object_build_datadescriptor(node, member, name) - elif isinstance(member, _CONSTANTS): + elif type(member) in _CONSTANTS: attach_const_node(node, name, member) else: # create an empty node so that the name is actually defined @@ -301,7 +298,7 @@ def imported_member(self, node, member, name): # Python 2.5.1 (r251:54863, Sep 1 2010, 22:03:14) # >>> print object.__new__.__module__ # None - modname = BUILTINS_MODULE + modname = BUILTINS else: attach_dummy_node(node, name, member) return True @@ -319,21 +316,21 @@ def imported_member(self, node, member, name): ### astng boot strapping ################################################### ### +ASTNG_BUILDER = InspectBuilder() _CONST_PROXY = {} def astng_boot_strapping(): """astng boot strapping the builtins module""" # this boot strapping is necessary since we need the Const nodes to # inspect_build builtins, and then we can proxy Const - builder = InspectBuilder() from ..common.compat import builtins - astng_builtin = builder.inspect_build(builtins) + astng_builtin = ASTNG_BUILDER.inspect_build(builtins) for cls, node_cls in CONST_CLS.items(): if cls is type(None): proxy = build_class('NoneType') proxy.parent = astng_builtin else: - proxy = astng_builtin.getattr(cls.__name__)[0] # XXX + proxy = astng_builtin.getattr(cls.__name__)[0] if cls in (dict, list, set, tuple): node_cls._proxied = proxy else: @@ -348,6 +345,7 @@ def _set_proxied(const): return _CONST_PROXY[const.value.__class__] Const._proxied = property(_set_proxied) -# FIXME : is it alright that Generator._proxied is not a astng node? -Generator._proxied = MANAGER.infer_astng_from_something(type(a for a in ())) +from types import GeneratorType +Generator._proxied = Class(GeneratorType.__name__, GeneratorType.__doc__) +ASTNG_BUILDER.object_build(Generator._proxied, GeneratorType) diff --git a/pylibs/pylama/pylint/logilab/astng/rebuilder.py b/pylibs/pylama/pylint/logilab/astng/rebuilder.py index f007bc90..03ee6128 100644 --- a/pylibs/pylama/pylint/logilab/astng/rebuilder.py +++ b/pylibs/pylama/pylint/logilab/astng/rebuilder.py @@ -32,7 +32,6 @@ Eq, Gt, GtE, In, Is, IsNot, Lt, LtE, NotEq, NotIn, ) -from .exceptions import ASTNGBuildingException from . import nodes as new @@ -698,7 +697,7 @@ def visit_return(self, node, parent): return newnode def visit_set(self, node, parent): - """visit a Tuple node by returning a fresh instance of it""" + """visit a Set node by returning a fresh instance of it""" newnode = new.Set() _lineno_parent(node, newnode, parent) newnode.elts = [self.visit(child, newnode) for child in node.elts] @@ -879,6 +878,8 @@ def visit_try(self, node, parent): newnode.set_line_info(newnode.last_child()) return newnode + def visit_yieldfrom(self, node, parent): + return self.visit_yield(node, parent) if sys.version_info >= (3, 0): TreeRebuilder = TreeRebuilder3k diff --git a/pylibs/pylama/pylint/logilab/astng/scoped_nodes.py b/pylibs/pylama/pylint/logilab/astng/scoped_nodes.py index 5f61511b..8e049775 100644 --- a/pylibs/pylama/pylint/logilab/astng/scoped_nodes.py +++ b/pylibs/pylama/pylint/logilab/astng/scoped_nodes.py @@ -1,7 +1,5 @@ -# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -31,15 +29,14 @@ from ..common.compat import builtins from ..common.decorators import cached -from . import BUILTINS_MODULE -from .exceptions import NotFoundError, NoDefault, \ +from .exceptions import NotFoundError, \ ASTNGBuildingException, InferenceError from .node_classes import Const, DelName, DelAttr, \ - Dict, From, List, Name, Pass, Raise, Return, Tuple, Yield, \ - are_exclusive, LookupMixIn, const_factory as cf, unpack_infer + Dict, From, List, Pass, Raise, Return, Tuple, Yield, \ + LookupMixIn, const_factory as cf, unpack_infer from .bases import NodeNG, InferenceContext, Instance,\ YES, Generator, UnboundMethod, BoundMethod, _infer_stmts, copy_context, \ - BUILTINS_NAME + BUILTINS from .mixins import FilterStmtsMixin from .bases import Statement from .manager import ASTNGManager @@ -268,7 +265,7 @@ def scope_lookup(self, node, name, offset=0): return self._scope_lookup(node, name, offset) def pytype(self): - return '%s.module' % BUILTINS_MODULE + return '%s.module' % BUILTINS def display_type(self): return 'Module' @@ -481,8 +478,8 @@ def __init__(self): def pytype(self): if 'method' in self.type: - return '%s.instancemethod' % BUILTINS_MODULE - return '%s.function' % BUILTINS_MODULE + return '%s.instancemethod' % BUILTINS + return '%s.function' % BUILTINS def display_type(self): if 'method' in self.type: @@ -608,14 +605,14 @@ def is_generator(self): """return true if this is a generator function""" # XXX should be flagged, not computed try: - return self.nodes_of_class(Yield, skip_klass=Function).next() + return self.nodes_of_class(Yield, skip_klass=(Function, Lambda)).next() except StopIteration: return False def infer_call_result(self, caller, context=None): """infer what a function is returning when called""" if self.is_generator(): - yield Generator(self) + yield Generator() return returns = self.nodes_of_class(Return, skip_klass=Function) for returnnode in returns: @@ -740,8 +737,8 @@ def block_range(self, lineno): def pytype(self): if self.newstyle: - return '%s.type' % BUILTINS_MODULE - return '%s.classobj' % BUILTINS_MODULE + return '%s.type' % BUILTINS + return '%s.classobj' % BUILTINS def display_type(self): return 'Class' @@ -872,15 +869,14 @@ def getattr(self, name, context=None): if name in self.special_attributes: if name == '__module__': return [cf(self.root().qname())] + values - # FIXME : what is expected by passing the list of ancestors to cf: - # you can just do [cf(tuple())] + values without breaking any test + # FIXME: do we really need the actual list of ancestors? + # returning [Tuple()] + values don't break any test # this is ticket http://www.logilab.org/ticket/52785 - if name == '__bases__': - return [cf(tuple(self.ancestors(recurs=False, context=context)))] + values # XXX need proper meta class handling + MRO implementation - if name == '__mro__' and self.newstyle: - # XXX mro is read-only but that's not our job to detect that - return [cf(tuple(self.ancestors(recurs=True, context=context)))] + values + if name == '__bases__' or (name == '__mro__' and self.newstyle): + node = Tuple() + node.items = self.ancestors(recurs=True, context=context) + return [node] + values return std_special_attributes(self, name) # don't modify the list in self.locals! values = list(values) @@ -932,7 +928,7 @@ def has_dynamic_getattr(self, context=None): #if self.newstyle: XXX cause an infinite recursion error try: getattribute = self.getattr('__getattribute__', context)[0] - if getattribute.root().name != BUILTINS_NAME: + if getattribute.root().name != BUILTINS: # class has a custom __getattribute__ defined return True except NotFoundError: diff --git a/pylibs/pylama/pylint/logilab/astng/utils.py b/pylibs/pylama/pylint/logilab/astng/utils.py index 73203299..325535b0 100644 --- a/pylibs/pylama/pylint/logilab/astng/utils.py +++ b/pylibs/pylama/pylint/logilab/astng/utils.py @@ -1,7 +1,5 @@ -# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# copyright 2003-2010 Sylvain Thenault, all rights reserved. -# contact mailto:thenault@gmail.com # # This file is part of logilab-astng. # @@ -24,6 +22,7 @@ __docformat__ = "restructuredtext en" from .exceptions import ASTNGBuildingException +from .builder import parse class ASTWalker: @@ -128,10 +127,6 @@ def _check_children(node): _check_children(child) -from _ast import PyCF_ONLY_AST -def parse(string): - return compile(string, "", 'exec', PyCF_ONLY_AST) - class TreeTester(object): '''A helper class to see _ast tree and compare with astng tree diff --git a/pylibs/pylama/pylint/reporters/__init__.py b/pylibs/pylama/pylint/reporters/__init__.py index 99cf2b7c..fa09c1ad 100644 --- a/pylibs/pylama/pylint/reporters/__init__.py +++ b/pylibs/pylama/pylint/reporters/__init__.py @@ -82,7 +82,10 @@ def encode(string): encoding = (getattr(self.out, 'encoding', None) or locale.getdefaultlocale()[1] or sys.getdefaultencoding()) - return string.encode(encoding) + # errors=replace, we don't want to crash when attempting to show + # source code line that can't be encoded with the current locale + # settings + return string.encode(encoding, 'replace') self.encode = encode def writeln(self, string=''): diff --git a/pylibs/pylama/pylint/reporters/text.py b/pylibs/pylama/pylint/reporters/text.py index 5b94dd69..c498e4ca 100644 --- a/pylibs/pylama/pylint/reporters/text.py +++ b/pylibs/pylama/pylint/reporters/text.py @@ -1,5 +1,4 @@ -# Copyright (c) 2003-2007 Sylvain Thenault (thenault@gmail.com). -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -23,7 +22,6 @@ """ import os -import sys from ..logilab.common.ureports import TextWriter from ..logilab.common.textutils import colorize_ansi diff --git a/pylibs/pylama/pylint/utils.py b/pylibs/pylama/pylint/utils.py index 532b686f..8cb79ed5 100644 --- a/pylibs/pylama/pylint/utils.py +++ b/pylibs/pylama/pylint/utils.py @@ -1,5 +1,4 @@ -# Copyright (c) 2003-2010 Sylvain Thenault (thenault@gmail.com). -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -22,8 +21,9 @@ from warnings import warn from os.path import dirname, basename, splitext, exists, isdir, join, normpath +from .logilab.common.interface import implements from .logilab.common.modutils import modpath_from_file, get_module_files, \ - file_from_modpath + file_from_modpath from .logilab.common.textutils import normalize_text from .logilab.common.configuration import rest_format_section from .logilab.common.ureports import Section @@ -31,6 +31,7 @@ from .logilab.astng import nodes, Module from .checkers import EmptyReport +from .interfaces import IRawChecker class UnknownMessage(Exception): @@ -60,6 +61,15 @@ class UnknownMessage(Exception): MSG_STATE_SCOPE_CONFIG = 0 MSG_STATE_SCOPE_MODULE = 1 + +# The line/node distinction does not apply to fatal errors and reports. +_SCOPE_EXEMPT = 'FR' + +class WarningScope(object): + LINE = 'line-based-msg' + NODE = 'node-based-msg' + + def sort_msgs(msgids): """sort message identifiers according to their category first""" msgs = {} @@ -95,7 +105,7 @@ def category_id(id): class Message: - def __init__(self, checker, msgid, msg, descr, symbol): + def __init__(self, checker, msgid, msg, descr, symbol, scope): assert len(msgid) == 5, 'Invalid message id %s' % msgid assert msgid[0] in MSG_TYPES, \ 'Bad message type %s in %r' % (msgid[0], msgid) @@ -104,6 +114,7 @@ def __init__(self, checker, msgid, msg, descr, symbol): self.descr = descr self.checker = checker self.symbol = symbol + self.scope = scope class MessagesHandlerMixIn: """a mix-in class containing all the messages related methods for the main @@ -121,6 +132,7 @@ def __init__(self): self._msgs_by_category = {} self.msg_status = 0 self._ignored_msgs = {} + self._suppression_mapping = {} def register_messages(self, checker): """register a dictionary of messages @@ -133,11 +145,18 @@ def register_messages(self, checker): """ msgs_dict = checker.msgs chkid = None + for msgid, msg_tuple in msgs_dict.iteritems(): - if len(msg_tuple) == 3: - (msg, msgsymbol, msgdescr) = msg_tuple + if implements(checker, IRawChecker): + scope = WarningScope.LINE + else: + scope = WarningScope.NODE + if len(msg_tuple) > 2: + (msg, msgsymbol, msgdescr) = msg_tuple[:3] assert msgsymbol not in self._messages_by_symbol, \ 'Message symbol %r is already defined' % msgsymbol + if len(msg_tuple) > 3 and 'scope' in msg_tuple[3]: + scope = msg_tuple[3]['scope'] else: # messages should have a symbol, but for backward compatibility # they may not. @@ -151,7 +170,7 @@ def register_messages(self, checker): assert chkid is None or chkid == msgid[1:3], \ 'Inconsistent checker part in message id %r' % msgid chkid = msgid[1:3] - msg = Message(checker, msgid, msg, msgdescr, msgsymbol) + msg = Message(checker, msgid, msg, msgdescr, msgsymbol, scope) self._messages[msgid] = msg self._messages_by_symbol[msgsymbol] = msg self._msgs_by_category.setdefault(msgid[0], []).append(msgid) @@ -320,6 +339,17 @@ def add_message(self, msgid, line=None, node=None, args=None): astng checkers should provide the node argument, raw checkers should provide the line argument. """ + msg_info = self._messages[msgid] + # Fatal messages and reports are special, the node/scope distinction + # does not apply to them. + if msgid[0] not in _SCOPE_EXEMPT: + if msg_info.scope == WarningScope.LINE: + assert node is None and line is not None, ( + 'Message %s must only provide line, got line=%s, node=%s' % (msgid, line, node)) + elif msg_info.scope == WarningScope.NODE: + # Node-based warnings may provide an override line. + assert node is not None, 'Message %s must provide Node, got None' + if line is None and node is not None: line = node.fromlineno if hasattr(node, 'col_offset'): @@ -340,8 +370,8 @@ def add_message(self, msgid, line=None, node=None, args=None): self.stats['by_msg'][msgid] += 1 except KeyError: self.stats['by_msg'][msgid] = 1 - msg = self._messages[msgid].msg # expand message ? + msg = msg_info.msg if args: msg %= args # get module and object diff --git a/pylibs/pylama/utils.py b/pylibs/pylama/utils.py index 1ab481fb..ec98e4e2 100644 --- a/pylibs/pylama/utils.py +++ b/pylibs/pylama/utils.py @@ -68,7 +68,7 @@ def pyflakes(path, code=None, **meta): def pylint(path, **meta): from sys import version_info - if version_info > (2, 8): + if version_info > (3, 0): import logging logging.warn("Pylint don't supported python3 and will be disabled.") return [] From 1c364b39c8e476bfe87b2dad2ac04c83fc801179 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 3 May 2013 18:30:47 +0800 Subject: [PATCH 006/582] Update autopep8 --- pylibs/autopep8.py | 521 +++++++++++++++++++++++++++++---------------- 1 file changed, 332 insertions(+), 189 deletions(-) diff --git a/pylibs/autopep8.py b/pylibs/autopep8.py index c024f21c..01d79ffe 100644 --- a/pylibs/autopep8.py +++ b/pylibs/autopep8.py @@ -22,7 +22,16 @@ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Automatically formats Python code to conform to the PEP 8 style guide.""" +"""Automatically formats Python code to conform to the PEP 8 style guide. + +Fixes that only need be done once can be added by adding a function of the form +"fix_(source)" to this module. They should return the fixed source code. +These fixes are picked up by apply_global_fixes(). + +Fixes that depend on pep8 should be added as methods to FixPEP8. See the class +documentation for more information. + +""" from __future__ import print_function from __future__ import division @@ -33,6 +42,7 @@ import inspect import os import re +import signal import sys try: from StringIO import StringIO @@ -44,7 +54,7 @@ import difflib import tempfile -from pylama import pep8 +import pep8 try: @@ -61,6 +71,9 @@ CRLF = '\r\n' +PYTHON_SHEBANG_REGEX = re.compile(r'^#!.*\bpython[23]?\b') + + # For generating line shortening candidates. SHORTEN_OPERATOR_GROUPS = frozenset([ frozenset([',']), @@ -70,6 +83,10 @@ ]) +DEFAULT_IGNORE = ','.join([pep8.DEFAULT_IGNORE, + 'W6']) + + def open_with_encoding(filename, encoding=None, mode='r'): """Return opened file with a specific encoding.""" if not encoding: @@ -102,6 +119,23 @@ def read_from_filename(filename, readlines=False): return input_file.readlines() if readlines else input_file.read() +def extended_blank_lines(logical_line, + blank_lines, + indent_level, + previous_logical): + """Check for missing blank lines after class declaration.""" + if (previous_logical.startswith('class ')): + if (logical_line.startswith(('def ', 'class ', '@')) or + pep8.DOCSTRING_REGEX.match(logical_line)): + if indent_level: + if not blank_lines: + yield (0, 'E301 expected 1 blank line, found 0') + elif previous_logical.startswith('def '): + if blank_lines and pep8.DOCSTRING_REGEX.match(logical_line): + yield (0, 'E303 too many blank lines ({0})'.format(blank_lines)) +pep8.register_check(extended_blank_lines) + + class FixPEP8(object): """Fix invalid code. @@ -122,7 +156,7 @@ class FixPEP8(object): [fixed method list] - e111 - - e121,e122,e123,e124,e125,e126,e127,e128 + - e121,e122,e123,e124,e125,e126,e127,e128,e129 - e201,e202,e203 - e211 - e221,e222,e223,e224,e225 @@ -135,10 +169,8 @@ class FixPEP8(object): - e502 - e701,e702 - e711 - - e721 - w291,w293 - w391 - - w602,w603,w604 """ @@ -152,11 +184,11 @@ def __init__(self, filename, options, contents=None): self.newline = find_newline(self.source) self.options = options self.indent_word = _get_indentword(unicode().join(self.source)) - self.logical_start = None - self.logical_end = None + # method definition self.fix_e111 = self.fix_e101 self.fix_e128 = self.fix_e127 + self.fix_e129 = self.fix_e125 self.fix_e202 = self.fix_e201 self.fix_e203 = self.fix_e201 self.fix_e211 = self.fix_e201 @@ -253,10 +285,8 @@ def fix_e101(self, _): else: return [] - def find_logical(self, force=False): + def _find_logical(self): # make a variable which is the index of all the starts of lines - if not force and self.logical_start is not None: - return logical_start = [] logical_end = [] last_newline = True @@ -281,8 +311,7 @@ def find_logical(self, force=False): parens += 1 elif t[1] in '}])': parens -= 1 - self.logical_start = logical_start - self.logical_end = logical_end + return (logical_start, logical_end) def _get_logical(self, result): """Return the logical line corresponding to the result. @@ -291,7 +320,7 @@ def _get_logical(self, result): """ try: - self.find_logical() + (logical_start, logical_end) = self._find_logical() except (IndentationError, tokenize.TokenError): return None @@ -299,33 +328,30 @@ def _get_logical(self, result): col = result['column'] - 1 ls = None le = None - for i in range(0, len(self.logical_start), 1): - x = self.logical_end[i] + for i in range(0, len(logical_start), 1): + x = logical_end[i] if x[0] > row or (x[0] == row and x[1] > col): le = x - ls = self.logical_start[i] + ls = logical_start[i] break if ls is None: return None original = self.source[ls[0]:le[0] + 1] return ls, le, original - def _fix_reindent(self, result, logical, fix_distinct=False): + def _fix_reindent(self, result, logical): """Fix a badly indented line. This is done by adding or removing from its initial indent only. """ - if not logical: - return [] + assert logical ls, _, original = logical - try: - rewrapper = Wrapper(original) - except (tokenize.TokenError, IndentationError): - return [] + + rewrapper = Wrapper(original) valid_indents = rewrapper.pep8_expected() if not rewrapper.rel_indent: - return [] + return [] # pragma: no cover if result['line'] > ls[0]: # got a valid continuation line number from pep8 row = result['line'] - ls[0] - 1 @@ -333,22 +359,10 @@ def _fix_reindent(self, result, logical, fix_distinct=False): valid = valid_indents[row] got = rewrapper.rel_indent[row] else: - # Line number from pep8 isn't a continuation line. Instead, - # compare our own function's result, look for the first mismatch, - # and just hope that we take fewer than 100 iterations to finish. - for row in range(0, len(original), 1): - valid = valid_indents[row] - got = rewrapper.rel_indent[row] - if valid != got: - break + return [] # pragma: no cover line = ls[0] + row # always pick the expected indent, for now. indent_to = valid[0] - if fix_distinct and indent_to == 4: - if len(valid) == 1: - return [] - else: - indent_to = valid[1] if got != indent_to: orig_line = self.source[line] @@ -359,7 +373,7 @@ def _fix_reindent(self, result, logical, fix_distinct=False): self.source[line] = new_line return [line + 1] # Line indexed at 1 else: - return [] + return [] # pragma: no cover def fix_e121(self, result, logical): """Fix indentation to be a multiple of four.""" @@ -374,8 +388,7 @@ def fix_e122(self, result, logical): def fix_e123(self, result, logical): """Align closing bracket to match opening bracket.""" # Fix by deleting whitespace to the correct level. - if not logical: - return [] + assert logical logical_lines = logical[2] line_index = result['line'] - 1 original_line = self.source[line_index] @@ -396,7 +409,7 @@ def fix_e124(self, result, logical): def fix_e125(self, result, logical): """Indent to distinguish line from next logical line.""" # Fix by indenting the line in error to the next stop. - modified_lines = self._fix_reindent(result, logical, fix_distinct=True) + modified_lines = self._fix_reindent(result, logical) if modified_lines: return modified_lines else: @@ -408,8 +421,7 @@ def fix_e125(self, result, logical): def fix_e126(self, result, logical): """Fix over-indented hanging indentation.""" # fix by deleting whitespace to the left - if not logical: - return [] + assert logical logical_lines = logical[2] line_index = result['line'] - 1 original = self.source[line_index] @@ -418,7 +430,7 @@ def fix_e126(self, result, logical): self.indent_word + original.lstrip()) if fixed == original: # Fall back to slower method. - return self._fix_reindent(result, logical) + return self._fix_reindent(result, logical) # pragma: no cover else: self.source[line_index] = fixed @@ -438,8 +450,7 @@ def _align_visual_indent(self, result, logical): This includes over (E127) and under (E128) indented lines. """ - if not logical: - return [] + assert logical logical_lines = logical[2] line_index = result['line'] - 1 original = self.source[line_index] @@ -449,11 +460,17 @@ def _align_visual_indent(self, result, logical): fixed = (_get_indentation(logical_lines[0]) + self.indent_word + original.lstrip()) else: + start_index = None for symbol in '([{': if symbol in logical_lines[0]: - fixed = logical_lines[0].find( - symbol) * ' ' + original.lstrip() - break + found_index = logical_lines[0].find(symbol) + if start_index is None: + start_index = found_index + else: + start_index = min(start_index, found_index) + + if start_index is not None: + fixed = start_index * ' ' + original.lstrip() if fixed == original: return [] @@ -506,6 +523,15 @@ def fix_e225(self, result): def fix_e231(self, result): """Add missing whitespace.""" + # Optimize for comma case. This will fix all commas in the full source + # code in one pass. + if ',' in result['info']: + original = ''.join(self.source) + new = refactor(original, ['ws_comma']) + if original.strip() != new.strip(): + self.source = [new] + return range(1, 1 + len(original)) + line_index = result['line'] - 1 target = self.source[line_index] offset = result['column'] @@ -598,9 +624,7 @@ def fix_e303(self, result): cnt = 0 line = result['line'] - 2 modified_lines = [] - while cnt < delete_linenum: - if line < 0: - break + while cnt < delete_linenum and line >= 0: if not self.source[line].strip(): self.source[line] = '' modified_lines.append(1 + line) # Line indexed at 1 @@ -669,12 +693,12 @@ def fix_e501(self, result): try: tokens = list(tokenize.generate_tokens(sio.readline)) except (tokenize.TokenError, IndentationError): - multi_line_candidate = break_multi_line( + multiline_candidate = break_multiline( target, newline=self.newline, indent_word=self.indent_word) - if multi_line_candidate: - self.source[line_index] = multi_line_candidate + if multiline_candidate: + self.source[line_index] = multiline_candidate return else: return [] @@ -695,11 +719,7 @@ def fix_e501(self, result): file=sys.stderr) for _candidate in candidates: - if _candidate is None: - continue - - if _candidate == target: - continue + assert _candidate is not None if (get_longest_length(_candidate, self.newline) >= get_longest_length(target, self.newline)): @@ -808,10 +828,6 @@ def fix_e712(self, result): self.source[line_index] = left + new_right - def fix_e721(self, _): - """Switch to use isinstance().""" - return self.refactor('idioms') - def fix_w291(self, result): """Remove trailing whitespace.""" fixed_line = self.source[result['line'] - 1].rstrip() @@ -836,47 +852,91 @@ def fix_w391(self, _): self.source = self.source[:original_length - blank_count] return range(1, 1 + original_length) - def refactor(self, fixer_name, ignore=None): - """Return refactored code using lib2to3. - Skip if ignore string is produced in the refactored code. +def fix_e26(source): + """Format block comments.""" + if '#' not in source: + # Optimization. + return source - """ - from lib2to3 import pgen2 - try: - new_text = refactor_with_2to3(''.join(self.source), - fixer_name=fixer_name) - except (pgen2.parse.ParseError, - UnicodeDecodeError, UnicodeEncodeError): - return [] + string_line_numbers = multiline_string_lines(source, + include_docstrings=True) + fixed_lines = [] + sio = StringIO(source) + for (line_number, line) in enumerate(sio.readlines(), start=1): + if (line.lstrip().startswith('#') and + line_number not in string_line_numbers): - original = unicode().join(self.source).strip() - if original == new_text.strip(): - return [] + indentation = _get_indentation(line) + line = line.lstrip() + + # Normalize beginning if not a shebang. + if len(line) > 1: + # Leave multiple spaces like '# ' alone. + if line.count('#') > 1 or line[1].isalnum(): + line = '# ' + line.lstrip('# \t') + + fixed_lines.append(indentation + line) else: - if ignore: - if ignore in new_text and ignore not in ''.join(self.source): - return [] - original_length = len(self.source) - self.source = [new_text] - return range(1, 1 + original_length) + fixed_lines.append(line) + + return ''.join(fixed_lines) - def fix_w601(self, _): - """Replace the {}.has_key() form with 'in'.""" - return self.refactor('has_key') - def fix_w602(self, _): - """Fix deprecated form of raising exception.""" - return self.refactor('raise', - ignore='with_traceback') +def refactor(source, fixer_names, ignore=None): + """Return refactored code using lib2to3. - def fix_w603(self, _): - """Replace <> with !=.""" - return self.refactor('ne') + Skip if ignore string is produced in the refactored code. - def fix_w604(self, _): - """Replace backticks with repr().""" - return self.refactor('repr') + """ + from lib2to3 import pgen2 + try: + new_text = refactor_with_2to3(source, + fixer_names=fixer_names) + except (pgen2.parse.ParseError, + UnicodeDecodeError, + UnicodeEncodeError, + IndentationError): + return source + + if ignore: + if ignore in new_text and ignore not in source: + return source + + return new_text + + +def fix_w602(source): + """Fix deprecated form of raising exception.""" + return refactor(source, ['raise'], + ignore='with_traceback') + + +def fix_w6(source): + """Fix various deprecated code (via lib2to3).""" + return refactor(source, + ['apply', + 'except', + 'exec', + 'execfile', + 'exitfunc', + 'has_key', + 'idioms', + 'import', + 'methodattrs', # Python >= 2.6 + 'ne', + 'numliterals', + 'operator', + 'paren', + 'reduce', + 'renames', + 'repr', + 'standarderror', + 'sys_exc', + 'throw', + 'tuple_params', + 'types', + 'xreadlines']) def find_newline(source): @@ -889,15 +949,13 @@ def find_newline(source): cr += 1 elif s.endswith(LF): lf += 1 - _max = max(cr, crlf, lf) + _max = max(lf, cr, crlf) if _max == lf: return LF elif _max == crlf: return CRLF - elif _max == cr: - return CR else: - return LF + return CR def _get_indentword(source): @@ -923,12 +981,24 @@ def _get_indentation(line): return '' -def _get_difftext(old, new, filename): +def get_diff_text(old, new, filename): + """Return text of unified diff between old and new.""" + newline = '\n' diff = difflib.unified_diff( old, new, 'original/' + filename, - 'fixed/' + filename) - return ''.join(diff) + 'fixed/' + filename, + lineterm=newline) + + text = '' + for line in diff: + text += line + + # Work around missing newline (http://bugs.python.org/issue2142). + if not line.endswith(newline): + text += newline + r'\ No newline at end of file' + newline + + return text def _priority_key(pep8_result): @@ -938,13 +1008,20 @@ def _priority_key(pep8_result): like indentation. """ - priority = ['e101', 'e111', 'w191', # Global fixes - 'e701', # Fix multiline colon-based before semicolon based - 'e702', # Break multiline statements early - 'e225', 'e231', # things that make lines longer - 'e201', # Remove extraneous whitespace before breaking lines - 'e501', # before we break lines - ] + priority = [ + # Global fixes. + 'e101', 'e111', 'w191', + # Fix multiline colon-based before semicolon based. + 'e701', + # Break multiline statements early. + 'e702', + # Things that make lines longer. + 'e225', 'e231', + # Remove extraneous whitespace before breaking lines. + 'e201', + # Before breaking lines. + 'e121', 'e122', 'e123', 'e124', 'e125', 'e126', 'e127', 'e128', 'e129', + ] key = pep8_result['id'].lower() if key in priority: return priority.index(key) @@ -1022,7 +1099,7 @@ def _shorten_line(tokens, source, indentation, indent_word, newline, fixed = first + newline + second # Only fix if syntax is okay. - if check_syntax(normalize_multiline(fixed) + if check_syntax(normalize_multiline(fixed, newline=newline) if aggressive else fixed): yield indentation + fixed @@ -1069,21 +1146,21 @@ def _shorten_line_at_tokens(tokens, source, indentation, indent_word, newline, assert fixed is not None - if check_syntax(normalize_multiline(fixed) + if check_syntax(normalize_multiline(fixed, newline=newline) if aggressive > 1 else fixed): return indentation + fixed else: return None -def normalize_multiline(line): +def normalize_multiline(line, newline): """Remove multiline-related code that will cause syntax error. This is for purposes of checking syntax. """ for quote in '\'"': - dict_pattern = r'^{q}[^{q}]*{q}\s*:\s*'.format(q=quote) + dict_pattern = r'^{q}[^{q}]*{q} *: *'.format(q=quote) if re.match(dict_pattern, line): if not line.strip().endswith('}'): line += '}' @@ -1091,7 +1168,8 @@ def normalize_multiline(line): if line.startswith('def ') and line.rstrip().endswith(':'): # Do not allow ':' to be alone. That is invalid. - if ':' not in line.split(): + split_line = [item.strip() for item in line.split(newline)] + if ':' not in split_line and 'def' not in split_line: return line[len('def'):].strip().rstrip(':') return line @@ -1166,7 +1244,7 @@ def __init__(self, input_text, newline): # Note that a line is all-blank iff it is a newline. self.lines = [] for line_number, line in enumerate(self.raw, start=1): - # Do not modify if inside a multi-line string. + # Do not modify if inside a multiline string. if line_number in self.string_content_line_numbers: self.lines.append(line) else: @@ -1212,7 +1290,7 @@ def run(self): # An indented comment line. If we saw the same # indentation before, reuse what it most recently # mapped to. - want = have2want.get(have, - 1) + want = have2want.get(have, -1) if want < 0: # Then it probably belongs to the next real stmt. for j in range(i + 1, len(stats) - 1): @@ -1395,21 +1473,20 @@ def pep8_expected(self): # What follows is an adjusted version of # pep8.py:continuation_line_indentation. All of the comments have been # stripped and the 'yield' statements replaced with 'pass'. - tokens = self.tokens - if not tokens: - return + if not self.tokens: + return # pragma: no cover - first_row = tokens[0][2][0] - nrows = 1 + tokens[-1][2][0] - first_row + first_row = self.tokens[0][2][0] + nrows = 1 + self.tokens[-1][2][0] - first_row # here are the return values valid_indents = [list()] * nrows - indent_level = tokens[0][2][1] + indent_level = self.tokens[0][2][1] valid_indents[0].append(indent_level) if nrows == 1: # bug, really. - return valid_indents + return valid_indents # pragma: no cover indent_next = self.logical_line.endswith(':') @@ -1435,7 +1512,7 @@ def pep8_expected(self): # use at the given indent level, and return the offset. This # algorithm is susceptible to "carried errors", but should # through repeated runs eventually solve indentation for - # multi-line expressions less than PEP8_PASSES_MAX lines long. + # multiline expressions less than PEP8_PASSES_MAX lines long. if depth: for open_row in range(row - 1, -1, -1): @@ -1558,22 +1635,20 @@ def _leading_space_count(line): return i -def refactor_with_2to3(source_text, fixer_name): +def refactor_with_2to3(source_text, fixer_names): """Use lib2to3 to refactor the source. Return the refactored source code. """ - from lib2to3 import refactor - fixers = ['lib2to3.fixes.fix_' + fixer_name] - tool = refactor.RefactoringTool( - fixer_names=fixers, - explicit=fixers) + from lib2to3.refactor import RefactoringTool + fixers = ['lib2to3.fixes.fix_' + name for name in fixer_names] + tool = RefactoringTool(fixer_names=fixers, explicit=fixers) return unicode(tool.refactor_string(source_text, name='')) -def break_multi_line(source_text, newline, indent_word): - """Break first line of multi-line code. +def break_multiline(source_text, newline, indent_word): + """Break first line of multiline code. Return None if a break is not possible. @@ -1583,9 +1658,10 @@ def break_multi_line(source_text, newline, indent_word): # Handle special case only. for symbol in '([{': # Only valid if symbol is not on a line by itself. - if (symbol in source_text - and source_text.rstrip().endswith(',') - and not source_text.lstrip().startswith(symbol)): + if (symbol in source_text and not source_text.strip() == symbol): + + if not source_text.rstrip()[-1] == ',': + continue index = 1 + source_text.find(symbol) @@ -1655,7 +1731,7 @@ def filter_results(source, results, aggressive=False): # Filter out incorrect E101 reports when there are no tabs. # pep8 will complain about this even if the tab indentation found - # elsewhere is in a multi-line string. + # elsewhere is in a multiline string. if issue_id == 'e101' and '\t' not in split_source[r['line']]: continue @@ -1719,34 +1795,13 @@ def shorten_comment(line, newline, max_line_length): initial_indent=indentation, subsequent_indent=indentation, width=max_line_length, - break_long_words=False) + break_long_words=False, + break_on_hyphens=False) return newline.join(split_lines) + newline else: return line + newline -def format_block_comments(source): - """Format block comments.""" - if '#' not in source: - # Optimization. - return source - - string_line_numbers = multiline_string_lines(source, - include_docstrings=True) - fixed_lines = [] - sio = StringIO(source) - for (line_number, line) in enumerate(sio.readlines(), start=1): - if (re.match(r'\s*#+\w+', line) and - line_number not in string_line_numbers): - fixed_lines.append(_get_indentation(line) + - '# ' + - line.lstrip().lstrip('#')) - else: - fixed_lines.append(line) - - return ''.join(fixed_lines) - - def normalize_line_endings(lines): """Return fixed line endings. @@ -1792,11 +1847,15 @@ def fix_lines(source_lines, options, filename=''): # Keep a history to break out of cycles. previous_hashes = set([hash(tmp_source)]) - fixed_source = tmp_source - if code_match('e26', select=options.select, ignore=options.ignore): - fixed_source = format_block_comments(fixed_source) + # Apply global fixes only once (for efficiency). + fixed_source = apply_global_fixes(tmp_source, options) + + passes = 0 + while True: + if options.pep8_passes >= 0 and passes > options.pep8_passes: + break + passes += 1 - for _ in range(-1, options.pep8_passes): tmp_source = copy.copy(fixed_source) fix = FixPEP8(filename, options, contents=tmp_source) @@ -1826,11 +1885,12 @@ def fix_file(filename, options=None, output=None): if options.diff: new = StringIO(fixed_source) new = new.readlines() - diff = _get_difftext(original_source, new, filename) + diff = get_diff_text(original_source, new, filename) if output: output.write(diff) + output.flush() else: - return output + return diff elif options.in_place: fp = open_with_encoding(filename, encoding=encoding, mode='w') @@ -1839,17 +1899,65 @@ def fix_file(filename, options=None, output=None): else: if output: output.write(fixed_source) + output.flush() else: return fixed_source +def global_fixes(): + """Yield multiple (code, function) tuples.""" + for function in globals().values(): + if inspect.isfunction(function): + arguments = inspect.getargspec(function)[0] + if arguments != ['source']: + continue + + code = extract_code_from_function(function) + if code: + yield (code, function) + + +def apply_global_fixes(source, options): + """Run global fixes on source code. + + Thsese are fixes that only need be done once (unlike those in FixPEP8, + which are dependent on pep8). + + """ + for (code, function) in global_fixes(): + if code_match(code, select=options.select, ignore=options.ignore): + if options.verbose: + print('---> Applying global fix for {0}'.format(code.upper()), + file=sys.stderr) + source = function(source) + + return source + + +def extract_code_from_function(function): + """Return code handled by function.""" + if not function.__name__.startswith('fix_'): + return None + + code = re.sub('^fix_', '', function.__name__) + if not code: + return None + + try: + int(code[1:]) + except ValueError: + return None + + return code + + def parse_args(args): """Parse command-line options.""" parser = OptionParser(usage='Usage: autopep8 [options] ' '[filename [filename ...]]' '\nUse filename \'-\' for stdin.', - version='autopep8: %s' % __version__, - description=__doc__, + version='%prog {0}'.format(__version__), + description=__doc__.split('\n')[0], prog='autopep8') parser.add_option('-v', '--verbose', action='count', dest='verbose', default=0, @@ -1866,9 +1974,9 @@ def parse_args(args): help='number of parallel jobs; ' 'match CPU count if value is less than 1') parser.add_option('-p', '--pep8-passes', metavar='n', - default=100, type=int, - help='maximum number of additional pep8 passes' - ' (default: %default)') + default=-1, type=int, + help='maximum number of additional pep8 passes ' + '(default: infinite)') parser.add_option('-a', '--aggressive', action='count', default=0, help='enable possibly unsafe changes (E711, E712); ' 'multiple -a result in more aggressive changes') @@ -1880,7 +1988,7 @@ def parse_args(args): 'used by --ignore and --select') parser.add_option('--ignore', metavar='errors', default='', help='do not fix these errors/warnings ' - '(default {0})'.format(pep8.DEFAULT_IGNORE)) + '(default: {0})'.format(DEFAULT_IGNORE)) parser.add_option('--select', metavar='errors', default='', help='fix only these errors/warnings (e.g. E4,W)') parser.add_option('--max-line-length', metavar='n', default=79, type=int, @@ -1920,8 +2028,12 @@ def parse_args(args): if options.ignore: options.ignore = options.ignore.split(',') - elif not options.select and pep8.DEFAULT_IGNORE: - options.ignore = pep8.DEFAULT_IGNORE.split(',') + elif not options.select: + if options.aggressive: + # Enable everything by default if aggressive. + options.select = ['E', 'W'] + else: + options.ignore = DEFAULT_IGNORE.split(',') if options.exclude: options.exclude = options.exclude.split(',') @@ -1954,6 +2066,10 @@ def supported_fixes(): re.sub(r'\s+', ' ', getattr(instance, attribute).__doc__)) + for (code, function) in global_fixes(): + yield (code.upper() + (4 - len(code)) * ' ', + re.sub(r'\s+', ' ', function.__doc__)) + def line_shortening_rank(candidate, newline, indent_word): """Return rank of candidate. @@ -1962,7 +2078,7 @@ def line_shortening_rank(candidate, newline, indent_word): """ rank = 0 - if candidate: + if candidate.strip(): lines = candidate.split(newline) offset = 0 @@ -2002,6 +2118,10 @@ def line_shortening_rank(candidate, newline, indent_word): if current_line.endswith('%'): rank -= 20 + + # Try to break list comprehensions at the "for". + if current_line.lstrip().startswith('for'): + rank -= 50 else: rank = 100000 @@ -2048,8 +2168,8 @@ def __init__(self, output): def write(self, s): self.__output.write(s.replace('\r\n', '\n').replace('\r', '\n')) - def __getattr__(self, key): - return getattr(self.__output, key) + def flush(self): + self.__output.flush() def temporary_file(): @@ -2062,16 +2182,16 @@ def temporary_file(): def match_file(filename, exclude): """Return True if file is okay for modifying/recursing.""" - if not filename.endswith('.py'): - return False - - if filename.startswith('.'): + if os.path.basename(filename).startswith('.'): return False for pattern in exclude: if fnmatch.fnmatch(filename, pattern): return False + if not is_python_file(filename): + return False + return True @@ -2083,9 +2203,8 @@ def find_files(filenames, recursive, exclude): for root, directories, children in os.walk(name): filenames += [os.path.join(root, f) for f in children if match_file(f, exclude)] - for d in directories: - if d.startswith('.'): - directories.remove(d) + directories[:] = [d for d in directories + if not d.startswith('.')] else: yield name @@ -2117,11 +2236,35 @@ def fix_multiple_files(filenames, options, output=None): _fix_file((name, options, output)) +def is_python_file(filename): + """Return True if filename is Python file.""" + if filename.endswith('.py'): + return True + + try: + with open_with_encoding(filename) as f: + first_line = f.readlines(1)[0] + except (IOError, IndexError): + return False + + if len(first_line) > 200: + # This is probably not even a text file. + return False + + if not PYTHON_SHEBANG_REGEX.match(first_line): + return False + + return True + + def main(): """Tool main.""" - if not pep8: - print('pep8 >= 1.3.2 required', file=sys.stderr) - return 1 + try: + # Exit on broken pipe. + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + except AttributeError: # pragma: no cover + # SIGPIPE is not available on Windows. + pass try: options, args = parse_args(sys.argv[1:]) @@ -2154,7 +2297,7 @@ def main(): fix_multiple_files(filenames, options, output) except KeyboardInterrupt: - return 1 + return 1 # pragma: no cover if __name__ == '__main__': From af87c01a62b7847a92227e5cabeb9456b7295c8c Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 3 May 2013 18:56:36 +0800 Subject: [PATCH 007/582] Added AUTHORS file. --- AUTHORS | 30 ++++++++++++++++++++++++++++++ Changelog.rst | 2 ++ README.rst | 4 +++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..d072603a --- /dev/null +++ b/AUTHORS @@ -0,0 +1,30 @@ +Maintainer: + +* Kirill Klenov + + +Contributors: + +* Pedro Algarvio (s0undt3ch); +* Alvin Francis (alvinfrancis); +* Boris Filippov (frenzykryger); +* Denis Kasak (dkasak); +* Steve Losh (sjl); +* nixon; +* tramchamploo; +* Benjamin Ruston (bruston); +* Robert David Grant (bgrant); +* Florent Xicluna (florentx); +* Piet Delport (pjdelport); +* lee (loyalpartner); +* Mohammed (mbadran); +* Ronald Andreu Kaiser (cathoderay); +* David Vogt (winged); +* Igor Guerrero (igorgue); +* Martin Brochhaus (mbrochh); +* Sorin Ionescu (sorin-ionescu); +* Dirk Wallenstein (dirkwallenstein); +* Jonathan McCall (Jonnymcc); +* Fredrik Henrysson (fhenrysson); +* Lowe Thiderman (thiderman); +* Naoya Inada (naoina); diff --git a/Changelog.rst b/Changelog.rst index a9984140..2256a6e5 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -5,6 +5,8 @@ Changelog -------------------- * Update `Pylint` to version 0.28.0; * Update `pyflakes` to version 0.7.3; +* Fixed `lint_ignore` options bug; +* Fixed encoding problems when code running; ## 2013-04-26 0.6.16 -------------------- diff --git a/README.rst b/README.rst index 9f9ab6e7..e672e006 100644 --- a/README.rst +++ b/README.rst @@ -469,13 +469,15 @@ at https://github.com/klen/python-mode/issues Contributing ============ +See in the `AUTHORS` file. + Development of pylint-mode happens at github: https://github.com/klen/python-mode Copyright ========= -Copyright (C) 2012 Kirill Klenov (klen_) +Copyright © 2013 Kirill Klenov (klen_) **Rope** Copyright (C) 2006-2010 Ali Gholami Rudi From cbfc8cd8fa19fc3643ddbaa6991fb4527f6a3f6f Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 3 May 2013 19:43:41 +0800 Subject: [PATCH 008/582] Update version. --- README.rst | 16 ---------------- doc/pymode.txt | 2 +- ftplugin/python/init-pymode.vim | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/README.rst b/README.rst index e672e006..2231f121 100644 --- a/README.rst +++ b/README.rst @@ -28,22 +28,6 @@ Another old presentation here: http://www.youtube.com/watch?v=YhqsjUUHj6g .. contents:: -Changelog -========= - -## 2013-03-15 0.6.12 --------------------- -* Update `PEP8` to version 1.4.5; -* Update `Pylint` to version 0.27.0; -* Update `autopep8` to version 0.8.7; -* Fix breakpoint definition; -* Update python syntax; -* Fixed run-time error when output non-ascii in multibyte locale; -* Move initialization into ftplugin as it is python specific; -* Pyrex (Cython) files support; -* Support `raw_input` in run python code; - - Requirements ============ diff --git a/doc/pymode.txt b/doc/pymode.txt index 9bf7d590..59cb3f94 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -6,7 +6,7 @@ (__) (__) (__) (_) (_)(_____)(_)\_) (_/\/\_)(_____)(____/(____) ~ - Version: 0.6.16 + Version: 0.6.17 ============================================================================== CONTENTS *Python-mode-contents* diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index e2733857..b8ea91c6 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -3,7 +3,7 @@ if exists('did_init_pymode_vim') endif let did_init_pymode_vim = 1 -let g:pymode_version = "0.6.16" +let g:pymode_version = "0.6.17" com! PymodeVersion echomsg "Current python-mode version: " . g:pymode_version From e10c7f71db06e60be11a7ccf9acf5c92e4a83cba Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Mon, 6 May 2013 15:47:23 +0800 Subject: [PATCH 009/582] Refactoring code checking --- autoload/pymode/lint.vim | 1 + ftplugin/python/init-pymode.vim | 3 +- pylibs/pymode/interface.py | 2 +- pylibs/pymode/lint.py | 39 +++++++-------- pylibs/pymode/queue.py | 88 ++++++++++++++++++--------------- 5 files changed, 71 insertions(+), 62 deletions(-) diff --git a/autoload/pymode/lint.vim b/autoload/pymode/lint.vim index 68782e18..082f80b8 100644 --- a/autoload/pymode/lint.vim +++ b/autoload/pymode/lint.vim @@ -15,6 +15,7 @@ fun! pymode#lint#Check() "{{{ let g:pymode_lint_buffer = bufnr('%') py from pymode import lint + py queue.stop_queue(False) py lint.check_file() endfunction " }}} diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index b8ea91c6..c948f191 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -1,7 +1,6 @@ -if exists('did_init_pymode_vim') +if pymode#Default('g:pymode_init', 1) finish endif -let did_init_pymode_vim = 1 let g:pymode_version = "0.6.17" diff --git a/pylibs/pymode/interface.py b/pylibs/pymode/interface.py index a7863bd0..3134882d 100644 --- a/pylibs/pymode/interface.py +++ b/pylibs/pymode/interface.py @@ -18,7 +18,7 @@ def get_current_buffer(): def show_message(message): - vim.command("call pymode#WideMessage('%s')" % message) + vim.command("call pymode#WideMessage('{0}')".format(message)) def command(cmd): diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index e2240876..6746332f 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -28,37 +28,36 @@ def check_file(): if s ]) - buffer = get_current_buffer() + buf = get_current_buffer() complexity = int(get_option('lint_mccabe_complexity') or 0) - add_task(run_checkers, checkers=checkers, ignore=ignore, - title='Code checking', - callback=parse_result, - buffer=buffer, - select=select, - complexity=complexity) + add_task( + run_checkers, + callback=parse_result, + title='Code checking', + checkers=checkers, + ignore=ignore, + buf=buf, + select=select, + complexity=complexity) -def run_checkers(task=None, checkers=None, ignore=None, - buffer=None, select=None, complexity=None): - buffer = (task and task.buffer) or buffer - filename = buffer.name +def run_checkers(checkers=None, ignore=None, buf=None, select=None, + complexity=None, callback=None): + + filename = buf.name result = [] pylint_options = '--rcfile={0} -r n'.format(get_var('lint_config')).split() - result = run(filename, ignore=ignore, select=select, linters=checkers, + return run(filename, ignore=ignore, select=select, linters=checkers, pylint=pylint_options, complexity=complexity) - if task: - task.result = result - task.finished = True - task.done = 100 - -def parse_result(result, bnum): - command(('let g:qf_list = {0}'.format(repr(result)).replace('\': u', '\': '))) - command('call pymode#lint#Parse({0})'.format(bnum)) +def parse_result(result, buf=None, **kwargs): + command(('let g:qf_list = {0}'.format(repr(result)).replace( + '\': u', '\': '))) + command('call pymode#lint#Parse({0})'.format(buf.number)) # pymode:lint_ignore=W0622 diff --git a/pylibs/pymode/queue.py b/pylibs/pymode/queue.py index ed90be8f..d1d087ae 100644 --- a/pylibs/pymode/queue.py +++ b/pylibs/pymode/queue.py @@ -1,60 +1,70 @@ import threading +from Queue import Queue, Empty + from .interface import show_message -import time + + +MAX_LIFE = 60 +CHECK_INTERVAL = .2 +RESULTS = Queue() class Task(threading.Thread): - def __init__(self, buffer, callback=None, title=None, *args, **kwargs): - self.buffer = buffer - self._stop = threading.Event() - self.result = None - self.callback = callback - self.done = 0 - self.finished = False - self.title = title + def __init__(self, *args, **kwargs): + self.stop = threading.Event() threading.Thread.__init__(self, *args, **kwargs) def run(self): - " Run tasks. " - self._Thread__target(task=self, *self._Thread__args, **self._Thread__kwargs) - - # Wait for result parsing - while not self.stopped(): - time.sleep(.2) + """ Run the task. + """ + try: + args, kwargs = self._Thread__args, self._Thread__kwargs + checking = self._Thread__target(*args, **kwargs) + if not self.stop.isSet(): + RESULTS.put((checking, args, kwargs)) - def stop(self): - " Stop task. " - self._stop.set() - - def stopped(self): - return self._stop.isSet() - - -def stop_queue(): - " Stop all tasks. " - for thread in threading.enumerate(): - if isinstance(thread, Task): - thread.stop() - show_message('%s stopped.' % thread.title) + except Exception as e: + if not self.stop.isSet(): + RESULTS.put(e) -def add_task(target, callback=None, buffer=None, title=None, *args, **kwargs): +def add_task(target, title=None, *args, **kwargs): " Add all tasks. " - task = Task(buffer, title=title, target=target, callback=callback, args=args, kwargs=kwargs) + task = Task(target=target, args=args, kwargs=kwargs) task.daemon = True task.start() - show_message('%s started.' % task.title) + show_message('{0} started.'.format(title)) -def check_task(): - " Check tasks for result. " +def stop_queue(message=True): + """ Stop all tasks. + """ + with RESULTS.mutex: + RESULTS.queue.clear() + for thread in threading.enumerate(): if isinstance(thread, Task): - if thread.finished: - thread.stop() - thread.callback(thread.result, thread.buffer.number) - else: - show_message('%s %s%%' % (thread.title, thread.done)) + thread.stop.set() + if message: + show_message("Task stopped.") + + +def check_task(): + """ Checking running tasks. + """ + try: + result = RESULTS.get(False) + assert isinstance(result, tuple) + except Empty: + return False + except AssertionError: + return False + result, _, kwargs = result + callback = kwargs.pop('callback') + callback(result, **kwargs) + + +# lint_ignore=W0703 From 29c8010f47a5d7b900c1d0ac81f4fa18df05b7c5 Mon Sep 17 00:00:00 2001 From: ikame Date: Fri, 10 May 2013 02:27:45 +0200 Subject: [PATCH 010/582] Several improvements Added option 'g:pymode_syntax_highlight_equal_operator' If set, assignment '=' operator is highlighted Added option 'g:pymode_syntax_highlight_stars_operator' If set, star '*' and double star '**' operators are highlighted Added option 'g:pymode_syntax_highlight_self' If set, 'self' and 'cls' are highlighted as keywords Added option 'g:pymode_syntax_builtin_types' If set, all built-in types are highlighted Some types like 'file' and 'super' were declared previously as built-in functions, but in reality they are also types. --- syntax/python.vim | 276 ++++++++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 121 deletions(-) diff --git a/syntax/python.vim b/syntax/python.vim index 7a8d06d3..e570b632 100644 --- a/syntax/python.vim +++ b/syntax/python.vim @@ -18,46 +18,70 @@ call pymode#Default('g:pymode_syntax_all', 1) " Keywords {{{ " ============ - syn keyword pythonStatement break continue del - syn keyword pythonStatement exec return - syn keyword pythonStatement pass raise - syn keyword pythonStatement global assert - syn keyword pythonStatement lambda yield - syn keyword pythonStatement with as - syn keyword pythonStatement def nextgroup=pythonFunction skipwhite - syn match pythonFunction "[a-zA-Z_][a-zA-Z0-9_]*" display contained - syn keyword pythonStatement class nextgroup=pythonClass skipwhite - syn match pythonClass "[a-zA-Z_][a-zA-Z0-9_]*" display contained - syn keyword pythonRepeat for while - syn keyword pythonConditional if elif else - syn keyword pythonInclude import from - syn keyword pythonException try except finally - syn keyword pythonOperator and in is not or + syn keyword pythonStatement break continue del + syn keyword pythonStatement exec return + syn keyword pythonStatement pass raise + syn keyword pythonStatement global nonlocal assert + syn keyword pythonStatement lambda yield + syn keyword pythonStatement with as + + syn keyword pythonStatement def nextgroup=pythonFunction skipwhite + syn match pythonFunction "\%(\%(def\s\|@\)\s*\)\@<=\h\%(\w\|\.\)*" contained nextgroup=pythonVars + syn region pythonVars start="(" end=")" contained contains=pythonParameters transparent keepend + syn match pythonParameters "[^,]*" contained contains=pythonExtraOperator,pythonParam skipwhite + syn match pythonParam "=[^,]*" contained contains=pythonBuiltinObj,pythonBuiltinType,pythonConstant,pythonStatement,pythonNumber,pythonString,pythonNumber,pythonBrackets skipwhite + syn match pythonBrackets "[(|)]" contained skipwhite + + syn keyword pythonStatement class nextgroup=pythonClass skipwhite + syn match pythonClass "\%(\%(class\s\)\s*\)\@<=\h\%(\w\|\.\)*" contained nextgroup=pythonClassVars + syn region pythonClassVars start="(" end=")" contained contains=pythonClassParameters transparent keepend + syn match pythonClassParameters "[^,\*]*" contained contains=pythonBuiltin,pythonBuiltinObj,pythonBuiltinType,pythonExtraOperatorpythonStatement,pythonBrackets,pythonString skipwhite + + syn keyword pythonRepeat for while + syn keyword pythonConditional if elif else + syn keyword pythonInclude import from + syn keyword pythonException try except finally + syn keyword pythonOperator and in is not or + + syn match pythonExtraOperator "\%([~!^&|/%+-]\|\%(class\s*\)\@\|<=\|\%(<\|\>\|>=\|=\@\|\.\.\.\|\.\.\|::\)" + syn match pythonExtraPseudoOperator "\%(-=\|/=\|\*\*=\|\*=\|&&=\|&=\|&&\|||=\||=\|||\|%=\|+=\|!\~\|!=\)" if !pymode#Default("g:pymode_syntax_print_as_function", 0) || !g:pymode_syntax_print_as_function syn keyword pythonStatement print endif + if !pymode#Default('g:pymode_syntax_highlight_equal_operator', g:pymode_syntax_all) || g:pymode_syntax_highlight_equal_operator + syn match pythonExtraOperator "\%(=\)" + endif + + if !pymode#Default('g:pymode_syntax_highlight_stars_operator', g:pymode_syntax_all) || g:pymode_syntax_highlight_stars_operator + syn match pythonExtraOperator "\%(\*\|\*\*\)" + endif + + if !pymode#Default('g:pymode_syntax_highlight_self', g:pymode_syntax_all) || g:pymode_syntax_highlight_self + syn keyword pythonSelf self cls + endif + " }}} " Decorators {{{ " ============== - syn match pythonDecorator "@" display nextgroup=pythonDottedName skipwhite + syn match pythonDecorator "@" display nextgroup=pythonDottedName skipwhite syn match pythonDottedName "[a-zA-Z_][a-zA-Z0-9_]*\(\.[a-zA-Z_][a-zA-Z0-9_]*\)*" display contained syn match pythonDot "\." display containedin=pythonDottedName - + " }}} " Comments {{{ " ============ - syn match pythonComment "#.*$" display contains=pythonTodo,@Spell - syn match pythonRun "\%^#!.*$" - syn match pythonCoding "\%^.*\(\n.*\)\?#.*coding[:=]\s*[0-9A-Za-z-_.]\+.*$" - syn keyword pythonTodo TODO FIXME XXX contained + syn match pythonComment "#.*$" display contains=pythonTodo,@Spell + syn match pythonRun "\%^#!.*$" + syn match pythonCoding "\%^.*\(\n.*\)\?#.*coding[:=]\s*[0-9A-Za-z-_.]\+.*$" + syn keyword pythonTodo TODO FIXME XXX contained " }}} @@ -65,19 +89,19 @@ call pymode#Default('g:pymode_syntax_all', 1) " Errors {{{ " ========== - syn match pythonError "\<\d\+\D\+\>" display - syn match pythonError "[$?]" display - syn match pythonError "[&|]\{2,}" display - syn match pythonError "[=]\{3,}" display + syn match pythonError "\<\d\+\D\+\>" display + syn match pythonError "[$?]" display + syn match pythonError "[&|]\{2,}" display + syn match pythonError "[=]\{3,}" display " Indent errors (mix space and tabs) if !pymode#Default('g:pymode_syntax_indent_errors', g:pymode_syntax_all) || g:pymode_syntax_indent_errors - syn match pythonIndentError "^\s*\( \t\|\t \)\s*\S"me=e-1 display + syn match pythonIndentError "^\s*\( \t\|\t \)\s*\S"me=e-1 display endif " Trailing space errors if !pymode#Default('g:pymode_syntax_space_errors', g:pymode_syntax_all) || g:pymode_syntax_space_errors - syn match pythonSpaceError "\s\+$" display + syn match pythonSpaceError "\s\+$" display endif " }}} @@ -86,58 +110,58 @@ call pymode#Default('g:pymode_syntax_all', 1) " Strings {{{ " =========== - syn region pythonString start=+[bB]\='+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonEscape,pythonEscapeError,@Spell - syn region pythonString start=+[bB]\="+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonEscape,pythonEscapeError,@Spell - syn region pythonString start=+[bB]\="""+ end=+"""+ keepend contains=pythonEscape,pythonEscapeError,pythonDocTest2,pythonSpaceError,@Spell - syn region pythonString start=+[bB]\='''+ end=+'''+ keepend contains=pythonEscape,pythonEscapeError,pythonDocTest,pythonSpaceError,@Spell + syn region pythonString start=+[bB]\='+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonEscape,pythonEscapeError,@Spell + syn region pythonString start=+[bB]\="+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonEscape,pythonEscapeError,@Spell + syn region pythonString start=+[bB]\="""+ end=+"""+ keepend contains=pythonEscape,pythonEscapeError,pythonDocTest2,pythonSpaceError,@Spell + syn region pythonString start=+[bB]\='''+ end=+'''+ keepend contains=pythonEscape,pythonEscapeError,pythonDocTest,pythonSpaceError,@Spell - syn match pythonEscape +\\[abfnrtv'"\\]+ display contained - syn match pythonEscape "\\\o\o\=\o\=" display contained - syn match pythonEscapeError "\\\o\{,2}[89]" display contained - syn match pythonEscape "\\x\x\{2}" display contained - syn match pythonEscapeError "\\x\x\=\X" display contained - syn match pythonEscape "\\$" + syn match pythonEscape +\\[abfnrtv'"\\]+ display contained + syn match pythonEscape "\\\o\o\=\o\=" display contained + syn match pythonEscapeError "\\\o\{,2}[89]" display contained + syn match pythonEscape "\\x\x\{2}" display contained + syn match pythonEscapeError "\\x\x\=\X" display contained + syn match pythonEscape "\\$" " Unicode - syn region pythonUniString start=+[uU]'+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,@Spell - syn region pythonUniString start=+[uU]"+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,@Spell - syn region pythonUniString start=+[uU]"""+ end=+"""+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,pythonDocTest2,pythonSpaceError,@Spell - syn region pythonUniString start=+[uU]'''+ end=+'''+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,pythonDocTest,pythonSpaceError,@Spell + syn region pythonUniString start=+[uU]'+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,@Spell + syn region pythonUniString start=+[uU]"+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,@Spell + syn region pythonUniString start=+[uU]"""+ end=+"""+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,pythonDocTest2,pythonSpaceError,@Spell + syn region pythonUniString start=+[uU]'''+ end=+'''+ keepend contains=pythonEscape,pythonUniEscape,pythonEscapeError,pythonUniEscapeError,pythonDocTest,pythonSpaceError,@Spell - syn match pythonUniEscape "\\u\x\{4}" display contained + syn match pythonUniEscape "\\u\x\{4}" display contained syn match pythonUniEscapeError "\\u\x\{,3}\X" display contained - syn match pythonUniEscape "\\U\x\{8}" display contained + syn match pythonUniEscape "\\U\x\{8}" display contained syn match pythonUniEscapeError "\\U\x\{,7}\X" display contained - syn match pythonUniEscape "\\N{[A-Z ]\+}" display contained - syn match pythonUniEscapeError "\\N{[^A-Z ]\+}" display contained + syn match pythonUniEscape "\\N{[A-Z ]\+}" display contained + syn match pythonUniEscapeError "\\N{[^A-Z ]\+}" display contained " Raw strings - syn region pythonRawString start=+[rR]'+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonRawEscape,@Spell - syn region pythonRawString start=+[rR]"+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonRawEscape,@Spell - syn region pythonRawString start=+[rR]"""+ end=+"""+ keepend contains=pythonDocTest2,pythonSpaceError,@Spell - syn region pythonRawString start=+[rR]'''+ end=+'''+ keepend contains=pythonDocTest,pythonSpaceError,@Spell + syn region pythonRawString start=+[rR]'+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonRawEscape,@Spell + syn region pythonRawString start=+[rR]"+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonRawEscape,@Spell + syn region pythonRawString start=+[rR]"""+ end=+"""+ keepend contains=pythonDocTest2,pythonSpaceError,@Spell + syn region pythonRawString start=+[rR]'''+ end=+'''+ keepend contains=pythonDocTest,pythonSpaceError,@Spell - syn match pythonRawEscape +\\['"]+ display transparent contained + syn match pythonRawEscape +\\['"]+ display transparent contained " Unicode raw strings - syn region pythonUniRawString start=+[uU][rR]'+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonRawEscape,pythonUniRawEscape,pythonUniRawEscapeError,@Spell - syn region pythonUniRawString start=+[uU][rR]"+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonRawEscape,pythonUniRawEscape,pythonUniRawEscapeError,@Spell - syn region pythonUniRawString start=+[uU][rR]"""+ end=+"""+ keepend contains=pythonUniRawEscape,pythonUniRawEscapeError,pythonDocTest2,pythonSpaceError,@Spell - syn region pythonUniRawString start=+[uU][rR]'''+ end=+'''+ keepend contains=pythonUniRawEscape,pythonUniRawEscapeError,pythonDocTest,pythonSpaceError,@Spell + syn region pythonUniRawString start=+[uU][rR]'+ skip=+\\\\\|\\'\|\\$+ excludenl end=+'+ end=+$+ keepend contains=pythonRawEscape,pythonUniRawEscape,pythonUniRawEscapeError,@Spell + syn region pythonUniRawString start=+[uU][rR]"+ skip=+\\\\\|\\"\|\\$+ excludenl end=+"+ end=+$+ keepend contains=pythonRawEscape,pythonUniRawEscape,pythonUniRawEscapeError,@Spell + syn region pythonUniRawString start=+[uU][rR]"""+ end=+"""+ keepend contains=pythonUniRawEscape,pythonUniRawEscapeError,pythonDocTest2,pythonSpaceError,@Spell + syn region pythonUniRawString start=+[uU][rR]'''+ end=+'''+ keepend contains=pythonUniRawEscape,pythonUniRawEscapeError,pythonDocTest,pythonSpaceError,@Spell - syn match pythonUniRawEscape "\([^\\]\(\\\\\)*\)\@<=\\u\x\{4}" display contained - syn match pythonUniRawEscapeError "\([^\\]\(\\\\\)*\)\@<=\\u\x\{,3}\X" display contained + syn match pythonUniRawEscape "\([^\\]\(\\\\\)*\)\@<=\\u\x\{4}" display contained + syn match pythonUniRawEscapeError "\([^\\]\(\\\\\)*\)\@<=\\u\x\{,3}\X" display contained " String formatting if !pymode#Default('g:pymode_syntax_string_formatting', g:pymode_syntax_all) || g:pymode_syntax_string_formatting - syn match pythonStrFormatting "%\(([^)]\+)\)\=[-#0 +]*\d*\(\.\d\+\)\=[hlL]\=[diouxXeEfFgGcrs%]" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString - syn match pythonStrFormatting "%[-#0 +]*\(\*\|\d\+\)\=\(\.\(\*\|\d\+\)\)\=[hlL]\=[diouxXeEfFgGcrs%]" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString + syn match pythonStrFormatting "%\(([^)]\+)\)\=[-#0 +]*\d*\(\.\d\+\)\=[hlL]\=[diouxXeEfFgGcrs%]" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString + syn match pythonStrFormatting "%[-#0 +]*\(\*\|\d\+\)\=\(\.\(\*\|\d\+\)\)\=[hlL]\=[diouxXeEfFgGcrs%]" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString endif " Str.format syntax if !pymode#Default('g:pymode_syntax_string_format', g:pymode_syntax_all) || g:pymode_syntax_string_format syn match pythonStrFormat "{{\|}}" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString - syn match pythonStrFormat "{\([a-zA-Z_][a-zA-Z0-9_]*\|\d\+\)\(\.[a-zA-Z_][a-zA-Z0-9_]*\|\[\(\d\+\|[^!:\}]\+\)\]\)*\(![rs]\)\=\(:\({\([a-zA-Z_][a-zA-Z0-9_]*\|\d\+\)}\|\([^}]\=[<>=^]\)\=[ +-]\=#\=0\=\d*\(\.\d\+\)\=[bcdeEfFgGnoxX%]\=\)\=\)\=}" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString + syn match pythonStrFormat "{\([a-zA-Z0-9_]*\|\d\+\)\(\.[a-zA-Z_][a-zA-Z0-9_]*\|\[\(\d\+\|[^!:\}]\+\)\]\)*\(![rs]\)\=\(:\({\([a-zA-Z_][a-zA-Z0-9_]*\|\d\+\)}\|\([^}]\=[<>=^]\)\=[ +-]\=#\=0\=\d*\(\.\d\+\)\=[bcdeEfFgGnoxX%]\=\)\=\)\=}" contained containedin=pythonString,pythonUniString,pythonRawString,pythonUniRawString endif " String templates @@ -149,8 +173,8 @@ call pymode#Default('g:pymode_syntax_all', 1) " DocTests if !pymode#Default('g:pymode_syntax_doctests', g:pymode_syntax_all) || g:pymode_syntax_doctests - syn region pythonDocTest start="^\s*>>>" end=+'''+he=s-1 end="^\s*$" contained - syn region pythonDocTest2 start="^\s*>>>" end=+"""+he=s-1 end="^\s*$" contained + syn region pythonDocTest start="^\s*>>>" end=+'''+he=s-1 end="^\s*$" contained + syn region pythonDocTest2 start="^\s*>>>" end=+"""+he=s-1 end="^\s*$" contained endif " }}} @@ -158,16 +182,16 @@ call pymode#Default('g:pymode_syntax_all', 1) " Numbers {{{ " =========== - syn match pythonHexError "\<0[xX]\x*[g-zG-Z]\x*[lL]\=\>" display - syn match pythonHexNumber "\<0[xX]\x\+[lL]\=\>" display + syn match pythonHexError "\<0[xX]\x*[g-zG-Z]\x*[lL]\=\>" display + syn match pythonHexNumber "\<0[xX]\x\+[lL]\=\>" display syn match pythonOctNumber "\<0[oO]\o\+[lL]\=\>" display syn match pythonBinNumber "\<0[bB][01]\+[lL]\=\>" display - syn match pythonNumber "\<\d\+[lLjJ]\=\>" display - syn match pythonFloat "\.\d\+\([eE][+-]\=\d\+\)\=[jJ]\=\>" display - syn match pythonFloat "\<\d\+[eE][+-]\=\d\+[jJ]\=\>" display - syn match pythonFloat "\<\d\+\.\d*\([eE][+-]\=\d\+\)\=[jJ]\=" display - syn match pythonOctError "\<0[oO]\=\o*[8-9]\d*[lL]\=\>" display - syn match pythonBinError "\<0[bB][01]*[2-9]\d*[lL]\=\>" display + syn match pythonNumber "\<\d\+[lLjJ]\=\>" display + syn match pythonFloat "\.\d\+\([eE][+-]\=\d\+\)\=[jJ]\=\>" display + syn match pythonFloat "\<\d\+[eE][+-]\=\d\+[jJ]\=\>" display + syn match pythonFloat "\<\d\+\.\d*\([eE][+-]\=\d\+\)\=[jJ]\=" display + syn match pythonOctError "\<0[oO]\=\o*[8-9]\d*[lL]\=\>" display + syn match pythonBinError "\<0[bB][01]*[2-9]\d*[lL]\=\>" display " }}} @@ -178,53 +202,53 @@ call pymode#Default('g:pymode_syntax_all', 1) if !pymode#Default('g:pymode_syntax_builtin_objs', g:pymode_syntax_all) || g:pymode_syntax_builtin_objs syn keyword pythonBuiltinObj True False Ellipsis None NotImplemented syn keyword pythonBuiltinObj __debug__ __doc__ __file__ __name__ __package__ - syn keyword pythonBuiltinObj self + endif + + if !pymode#Default('g:pymode_syntax_builtin_types', g:pymode_syntax_all) || g:pymode_syntax_builtin_types + syn keyword pythonBuiltinType type object + syn keyword pythonBuiltinType str basestring unicode buffer bytearray bytes chr unichr + syn keyword pythonBuiltinType dict int long bool float complex set frozenset list tuple + syn keyword pythonBuiltinType file super endif " Builtin functions if !pymode#Default('g:pymode_syntax_builtin_funcs', g:pymode_syntax_all) || g:pymode_syntax_builtin_funcs - syn keyword pythonBuiltinFunc __import__ abs all any apply - syn keyword pythonBuiltinFunc basestring bin bool buffer bytearray bytes callable - syn keyword pythonBuiltinFunc chr classmethod cmp coerce compile complex - syn keyword pythonBuiltinFunc delattr dict dir divmod enumerate eval - syn keyword pythonBuiltinFunc execfile file filter float format frozenset getattr - syn keyword pythonBuiltinFunc globals hasattr hash help hex id - syn keyword pythonBuiltinFunc input int intern isinstance - syn keyword pythonBuiltinFunc issubclass iter len list locals long map max - syn keyword pythonBuiltinFunc min next object oct open ord - syn keyword pythonBuiltinFunc pow property range - syn keyword pythonBuiltinFunc raw_input reduce reload repr - syn keyword pythonBuiltinFunc reversed round set setattr - syn keyword pythonBuiltinFunc slice sorted staticmethod str sum super tuple - syn keyword pythonBuiltinFunc type unichr unicode vars xrange zip + syn keyword pythonBuiltinFunc __import__ abs all any apply + syn keyword pythonBuiltinFunc bin callable classmethod cmp coerce compile + syn keyword pythonBuiltinFunc delattr dir divmod enumerate eval execfile filter + syn keyword pythonBuiltinFunc format getattr globals locals hasattr hash help hex id + syn keyword pythonBuiltinFunc input intern isinstance issubclass iter len map max min + syn keyword pythonBuiltinFunc next oct open ord pow property range xrange + syn keyword pythonBuiltinFunc raw_input reduce reload repr reversed round setattr + syn keyword pythonBuiltinFunc slice sorted staticmethod sum vars zip if pymode#Default('g:pymode_syntax_print_as_function', 0) && g:pymode_syntax_print_as_function - syn keyword pythonBuiltinFunc print + syn keyword pythonBuiltinFunc print endif endif " Builtin exceptions and warnings if !pymode#Default('g:pymode_syntax_highlight_exceptions', g:pymode_syntax_all) || g:pymode_syntax_highlight_exceptions - syn keyword pythonExClass BaseException - syn keyword pythonExClass Exception StandardError ArithmeticError - syn keyword pythonExClass LookupError EnvironmentError - syn keyword pythonExClass AssertionError AttributeError BufferError EOFError - syn keyword pythonExClass FloatingPointError GeneratorExit IOError - syn keyword pythonExClass ImportError IndexError KeyError - syn keyword pythonExClass KeyboardInterrupt MemoryError NameError - syn keyword pythonExClass NotImplementedError OSError OverflowError - syn keyword pythonExClass ReferenceError RuntimeError StopIteration - syn keyword pythonExClass SyntaxError IndentationError TabError - syn keyword pythonExClass SystemError SystemExit TypeError - syn keyword pythonExClass UnboundLocalError UnicodeError - syn keyword pythonExClass UnicodeEncodeError UnicodeDecodeError - syn keyword pythonExClass UnicodeTranslateError ValueError VMSError - syn keyword pythonExClass WindowsError ZeroDivisionError - syn keyword pythonExClass Warning UserWarning BytesWarning DeprecationWarning - syn keyword pythonExClass PendingDepricationWarning SyntaxWarning - syn keyword pythonExClass RuntimeWarning FutureWarning - syn keyword pythonExClass ImportWarning UnicodeWarning + syn keyword pythonExClass BaseException + syn keyword pythonExClass Exception StandardError ArithmeticError + syn keyword pythonExClass LookupError EnvironmentError + syn keyword pythonExClass AssertionError AttributeError BufferError EOFError + syn keyword pythonExClass FloatingPointError GeneratorExit IOError + syn keyword pythonExClass ImportError IndexError KeyError + syn keyword pythonExClass KeyboardInterrupt MemoryError NameError + syn keyword pythonExClass NotImplementedError OSError OverflowError + syn keyword pythonExClass ReferenceError RuntimeError StopIteration + syn keyword pythonExClass SyntaxError IndentationError TabError + syn keyword pythonExClass SystemError SystemExit TypeError + syn keyword pythonExClass UnboundLocalError UnicodeError + syn keyword pythonExClass UnicodeEncodeError UnicodeDecodeError + syn keyword pythonExClass UnicodeTranslateError ValueError VMSError + syn keyword pythonExClass WindowsError ZeroDivisionError + syn keyword pythonExClass Warning UserWarning BytesWarning DeprecationWarning + syn keyword pythonExClass PendingDepricationWarning SyntaxWarning + syn keyword pythonExClass RuntimeWarning FutureWarning + syn keyword pythonExClass ImportWarning UnicodeWarning endif " }}} @@ -245,31 +269,40 @@ endif hi def link pythonStatement Statement hi def link pythonInclude Include - hi def link pythonFunction Function + hi def link pythonFunction Function + hi def link pythonClass Type + hi def link pythonParameters Normal + hi def link pythonParam Normal + hi def link pythonBrackets Normal + hi def link pythonClassParameters Normal + hi def link pythonSelf Identifier + hi def link pythonConditional Conditional hi def link pythonRepeat Repeat hi def link pythonException Exception - hi def link pythonOperator Operator + hi def link pythonOperator Operator + hi def link pythonExtraOperator Operator + hi def link pythonExtraPseudoOperator Operator hi def link pythonDecorator Define hi def link pythonDottedName Function hi def link pythonDot Normal - hi def link pythonComment Comment - hi def link pythonCoding Special - hi def link pythonRun Special - hi def link pythonTodo Todo + hi def link pythonComment Comment + hi def link pythonCoding Special + hi def link pythonRun Special + hi def link pythonTodo Todo - hi def link pythonError Error + hi def link pythonError Error hi def link pythonIndentError Error hi def link pythonSpaceError Error - hi def link pythonString String + hi def link pythonString String hi def link pythonUniString String hi def link pythonRawString String hi def link pythonUniRawString String - hi def link pythonEscape Special + hi def link pythonEscape Special hi def link pythonEscapeError Error hi def link pythonUniEscape Special hi def link pythonUniEscapeError Error @@ -280,21 +313,22 @@ endif hi def link pythonStrFormat Special hi def link pythonStrTemplate Special - hi def link pythonDocTest Special - hi def link pythonDocTest2 Special + hi def link pythonDocTest Special + hi def link pythonDocTest2 Special - hi def link pythonNumber Number + hi def link pythonNumber Number hi def link pythonHexNumber Number hi def link pythonOctNumber Number hi def link pythonBinNumber Number - hi def link pythonFloat Float - hi def link pythonOctError Error - hi def link pythonHexError Error - hi def link pythonBinError Error + hi def link pythonFloat Float + hi def link pythonOctError Error + hi def link pythonHexError Error + hi def link pythonBinError Error + hi def link pythonBuiltinType Type hi def link pythonBuiltinObj Structure hi def link pythonBuiltinFunc Function - hi def link pythonExClass Structure + hi def link pythonExClass Structure " }}} From ba340fb74a5d0c316ef77c030a37a7746b0f9bba Mon Sep 17 00:00:00 2001 From: alvinfrancis Date: Mon, 13 May 2013 00:48:29 +0800 Subject: [PATCH 011/582] Fix error on non-ascii characters in docstrings Fix for issue #211: 'Non-ascii characters in docstrings trigger unicode error'. Use _get_encoding to encode docs in show_doc. --- pylibs/ropevim.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pylibs/ropevim.py b/pylibs/ropevim.py index 2912a170..4fd15c07 100644 --- a/pylibs/ropevim.py +++ b/pylibs/ropevim.py @@ -268,9 +268,8 @@ def _writedefs(self, locations, filename): def show_doc(self, docs, altview=False): if docs: - vim.command( - 'call pymode#ShowStr("{0}")'.format(docs.replace('"', '\\"')) - ) + docs = docs.encode(self._get_encoding()).replace('"', '\\"') + vim.command('call pymode#ShowStr("{0}")'.format(docs)) def preview_changes(self, diffs): echo(diffs) From b60cdf8601e9533122e2fe61082064fd0f819f47 Mon Sep 17 00:00:00 2001 From: alvinfrancis Date: Mon, 13 May 2013 00:50:11 +0800 Subject: [PATCH 012/582] Use _get_encoding for other instances of encode --- pylibs/ropevim.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pylibs/ropevim.py b/pylibs/ropevim.py index 4fd15c07..9692c828 100644 --- a/pylibs/ropevim.py +++ b/pylibs/ropevim.py @@ -379,8 +379,7 @@ def done(self): def echo(message): - if isinstance(message, unicode): - message = message.encode(vim.eval('&encoding')) + message = message.encode(VimUtils._get_encoding()) print message @@ -388,8 +387,7 @@ def status(message): if _rope_quiet: return - if isinstance(message, unicode): - message = message.encode(vim.eval('&encoding')) + message = message.encode(VimUtils._get_encoding()) vim.command('redraw | echon "{0}"'.format(message)) From 0a226b82555122d526bc9be79ac2474196e19e1f Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 15 May 2013 14:44:16 +0800 Subject: [PATCH 013/582] Update pylama --- pylibs/pylama/__init__.py | 2 +- pylibs/pylama/pep8.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index cebb3212..71308acd 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,6 +1,6 @@ " pylama -- Python code audit. " -version_info = (0, 3, 1) +version_info = (0, 3, 2) __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/pep8.py b/pylibs/pylama/pep8.py index e586c1d7..602466d9 100644 --- a/pylibs/pylama/pep8.py +++ b/pylibs/pylama/pep8.py @@ -842,19 +842,21 @@ def compound_statements(logical_line): line = logical_line last_char = len(line) - 1 found = line.find(':') - if -1 < found < last_char: + while -1 < found < last_char: 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(':', found + 1) found = line.find(';') - if -1 < found: + while -1 < found: if found < last_char: yield found, "E702 multiple statements on one line (semicolon)" else: yield found, "E703 statement ends with a semicolon" + found = line.find(';', found + 1) def explicit_line_join(logical_line, tokens): @@ -1016,8 +1018,6 @@ def readlines(filename): return f.readlines() finally: f.close() - - BOM_UTF8 = '\xef\xbb\xbf' isidentifier = re.compile(r'[a-zA-Z_]\w*').match stdin_get_value = sys.stdin.read else: @@ -1035,8 +1035,6 @@ def readlines(filename): return f.readlines() finally: f.close() - - BOM_UTF8 = '\ufeff' isidentifier = str.isidentifier def stdin_get_value(): @@ -1202,8 +1200,13 @@ def __init__(self, filename=None, lines=None, self.lines = [] else: self.lines = lines - if self.lines and self.lines[0].startswith(BOM_UTF8): - self.lines[0] = self.lines[0][len(BOM_UTF8):] + if self.lines: + ord0 = ord(self.lines[0][0]) + if ord0 in (0xef, 0xfeff): # Strip the UTF-8 BOM + if ord0 == 0xfeff: + self.lines[0] = self.lines[0][1:] + elif self.lines[0][:3] == '\xef\xbb\xbf': + self.lines[0] = self.lines[0][3:] self.report = report or options.report self.report_error = self.report.error From 5e3cb8b9b7042ecf3e006fe6f7f7cfd88ce57125 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 15 May 2013 14:47:05 +0800 Subject: [PATCH 014/582] Update autopep8 --- pylibs/autopep8.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/pylibs/autopep8.py b/pylibs/autopep8.py index 01d79ffe..0cef7623 100644 --- a/pylibs/autopep8.py +++ b/pylibs/autopep8.py @@ -54,7 +54,7 @@ import difflib import tempfile -import pep8 +from pylama import pep8 try: @@ -63,7 +63,7 @@ unicode = str -__version__ = '0.8.7' +__version__ = '0.9' CR = '\r' @@ -83,8 +83,7 @@ ]) -DEFAULT_IGNORE = ','.join([pep8.DEFAULT_IGNORE, - 'W6']) +DEFAULT_IGNORE = 'E24,W6' def open_with_encoding(filename, encoding=None, mode='r'): @@ -716,7 +715,10 @@ def fix_e501(self, result): if self.options.verbose >= 4: print(('-' * 79 + '\n').join([''] + candidates + ['']), - file=sys.stderr) + file=codecs.getwriter('utf-8')(sys.stderr.buffer + if hasattr(sys.stderr, + 'buffer') + else sys.stderr)) for _candidate in candidates: assert _candidate is not None @@ -1877,9 +1879,16 @@ def fix_file(filename, options=None, output=None): fixed_source = original_source - if options.in_place: + if options.in_place or output: encoding = detect_encoding(filename) + if output: + output = codecs.getwriter(encoding)(output.buffer + if hasattr(output, 'buffer') + else output) + + output = LineEndingWrapper(output) + fixed_source = fix_lines(fixed_source, options, filename=filename) if options.diff: @@ -1978,7 +1987,7 @@ def parse_args(args): help='maximum number of additional pep8 passes ' '(default: infinite)') parser.add_option('-a', '--aggressive', action='count', default=0, - help='enable possibly unsafe changes (E711, E712); ' + help='enable non-whitespace changes; ' 'multiple -a result in more aggressive changes') parser.add_option('--exclude', metavar='globs', help='exclude files/directories that match these ' @@ -2066,7 +2075,7 @@ def supported_fixes(): re.sub(r'\s+', ' ', getattr(instance, attribute).__doc__)) - for (code, function) in global_fixes(): + for (code, function) in sorted(global_fixes()): yield (code.upper() + (4 - len(code)) * ' ', re.sub(r'\s+', ' ', function.__doc__)) @@ -2289,13 +2298,7 @@ def main(): else: filenames = args[:1] - output = codecs.getwriter('utf-8')(sys.stdout.buffer - if sys.version_info[0] >= 3 - else sys.stdout) - - output = LineEndingWrapper(output) - - fix_multiple_files(filenames, options, output) + fix_multiple_files(filenames, options, sys.stdout) except KeyboardInterrupt: return 1 # pragma: no cover From 70efc4c18a65a4d4347c28bff477485a5487dcee Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 15 May 2013 14:51:43 +0800 Subject: [PATCH 015/582] Update changelog --- Changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.rst b/Changelog.rst index 2256a6e5..8e1bc6ea 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,6 +1,11 @@ Changelog ========= +## 2013-05-15 0.6.18 +-------------------- +* Fixed autopep8 (`PyLintAuto`) command; +* Fix error on non-ascii characters in docstrings; + ## 2013-05-03 0.6.17 -------------------- * Update `Pylint` to version 0.28.0; From 11ec7b403416d8ce2e3010bdd181355c73ccfccf Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 15 May 2013 15:19:07 +0800 Subject: [PATCH 016/582] Update docs --- README.rst | 9 ++++ doc/pymode.txt | 135 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/README.rst b/README.rst index 2231f121..4b5f89ff 100644 --- a/README.rst +++ b/README.rst @@ -329,6 +329,15 @@ Default values: :: " Highlight exceptions let g:pymode_syntax_highlight_exceptions = g:pymode_syntax_all + " Highlight equal operator + let g:pymode_syntax_highlight_equal_operator = g:pymode_syntax_all + + " Highlight stars operator + let g:pymode_syntax_highlight_stars_operator = g:pymode_syntax_all + + " Highlight `self` + let g:pymode_syntax_highlight_self = g:pymode_syntax_all + " For fast machines let g:pymode_syntax_slow_sync = 0 diff --git a/doc/pymode.txt b/doc/pymode.txt index 59cb3f94..124fbba0 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -100,6 +100,36 @@ PythonMode. These options should be set in your vimrc. |'pymode_syntax'| Turns off the custom syntax highlighting +|'pymode_syntax_all'| Enable all hightlight groups + +|'pymode_syntax_print_as_function'| Hightlight `print` as function + +|'pymode_syntax_highlight_equal_operator'| Hightlight `=` + +|'pymode_syntax_highlight_stars_operator'| Hightlight `*` + +|'pymode_syntax_highlight_self'| Hightlight `self` + +|'pymode_syntax_indent_errors'| Hightlight indentation errors + +|'pymode_syntax_space_errors'| Hightlight trailing spaces as errors + +|'pymode_syntax_string_formating'| Hightlight string formating + +|'pymode_syntax_string_format'| Hightlight Str.format syntax + +|'pymode_syntax_string_templates'| Hightlight string templates + +|'pymode_syntax_doc_tests'| Hightlight doctests + +|'pymode_syntax_builtin_objs'| Hightlight builtin objects + +|'pymode_syntax_builtin_types'| Hightlight builtin types + +|'pymode_syntax_builtin_functions'| Hightlight builtin functions + +|'pymode_syntax_highlight_exceptions'| Hightlight builtin exceptions + |'pymode_indent'| Enable/Disable pymode PEP8 indentation |'pymode_options'| Set default pymode options for @@ -344,6 +374,111 @@ Default: 1. If this option is set to 0 then the custom syntax highlighting will not be used. +------------------------------------------------------------------------------ + *'pymode_syntax_all'* +Values: 0 or 1. +Default: 1. + +Enabling all hightlight groups. + +------------------------------------------------------------------------------ + *'pymode_syntax_print_as_function'* +Values: 0 or 1. +Default: 0. + +Hightlight `print` as function + +------------------------------------------------------------------------------ + *'pymode_syntax_highlight_equal_operator'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight `=` + +------------------------------------------------------------------------------ + *'pymode_syntax_highlight_stars_operator'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight `*` + +------------------------------------------------------------------------------ + *'pymode_syntax_highlight_self'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight `self` + +------------------------------------------------------------------------------ + *'pymode_syntax_indent_errors'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight indentation errors + +------------------------------------------------------------------------------ + *'pymode_syntax_space_errors'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight trailing spaces as errors + +------------------------------------------------------------------------------ + *'pymode_syntax_string_formating'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight string formating + +------------------------------------------------------------------------------ + *'pymode_syntax_string_format'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight Str.format syntax + +------------------------------------------------------------------------------ + *'pymode_syntax_string_templates'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight string templates + +------------------------------------------------------------------------------ + *'pymode_syntax_string_doctests'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight doctests + +------------------------------------------------------------------------------ + *'pymode_syntax_builtin_objs'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight builtin objects + +------------------------------------------------------------------------------ + *'pymode_syntax_builtin_types'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight builtin types + +------------------------------------------------------------------------------ + *'pymode_syntax_builtin_functions'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight builtin functions + +------------------------------------------------------------------------------ + *'pymode_syntax_highlight_exceptions'* +Values: 0 or 1. +Default: |'pymode_syntax_all'|. + +Hightlight builtin exceptions + ------------------------------------------------------------------------------ *'pymode_indent'* Values: 0 or 1. From 3f75e294a0b2254f0ed8732cf09116011fe9e096 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 15 May 2013 15:20:36 +0800 Subject: [PATCH 017/582] Update changelog --- AUTHORS | 1 + Changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index d072603a..e0bc5bbb 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ Contributors: * Fredrik Henrysson (fhenrysson); * Lowe Thiderman (thiderman); * Naoya Inada (naoina); +* Anler Hp (ikame); diff --git a/Changelog.rst b/Changelog.rst index 8e1bc6ea..e3d57589 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -5,6 +5,7 @@ Changelog -------------------- * Fixed autopep8 (`PyLintAuto`) command; * Fix error on non-ascii characters in docstrings; +* Update python syntax; ## 2013-05-03 0.6.17 -------------------- From cb5d970d4d51ff68546614c119c2f5ab88099848 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 15 May 2013 15:21:16 +0800 Subject: [PATCH 018/582] Update version --- doc/pymode.txt | 2 +- ftplugin/python/init-pymode.vim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pymode.txt b/doc/pymode.txt index 124fbba0..b2778909 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -6,7 +6,7 @@ (__) (__) (__) (_) (_)(_____)(_)\_) (_/\/\_)(_____)(____/(____) ~ - Version: 0.6.17 + Version: 0.6.18 ============================================================================== CONTENTS *Python-mode-contents* diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index c948f191..dc8d6f56 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -2,7 +2,7 @@ if pymode#Default('g:pymode_init', 1) finish endif -let g:pymode_version = "0.6.17" +let g:pymode_version = "0.6.18" com! PymodeVersion echomsg "Current python-mode version: " . g:pymode_version From c33b13534478944041098cb1f663c2d96cab9897 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 24 May 2013 11:06:48 +0800 Subject: [PATCH 019/582] Add g:pymode_rope_autocomplete_map option. --- README.rst | 3 +++ doc/pymode.txt | 8 ++++---- ftplugin/python/init-pymode.vim | 4 ++++ ftplugin/python/pymode.vim | 4 ++-- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 4b5f89ff..8d7a58c9 100644 --- a/README.rst +++ b/README.rst @@ -203,6 +203,9 @@ Default values: :: " Load rope plugin let g:pymode_rope = 1 + " Map keys for autocompletion + let g:pymode_rope_autocomplete_map = '' + " Auto create and open ropeproject let g:pymode_rope_auto_project = 1 diff --git a/doc/pymode.txt b/doc/pymode.txt index b2778909..af8917b8 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -539,10 +539,10 @@ To redefine keys, see: |PythonModeOptions| ================ ============================ Key Command ================ ============================ -K Show python docs for current word under cursor -C-Space Rope code assist -r Run current buffer -b Set breakpoints +K Show python docs for current word under cursor (`g:pymode_doc_key`) +C-Space Rope code assist (`g:pymode_rope_autocomplete_map`) +r Run current buffer (`g:pymode_run_key`) +b Set breakpoints (`g:pymode_breakpoint_key`) [[ Jump to previous class or function (normal, visual, operator modes) ]] Jump to next class or function (normal, visual, operator modes) [M Jump to previous class or method (normal, visual, operator modes) diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index dc8d6f56..be4524b5 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -189,6 +189,10 @@ endif if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope + " OPTION: g:pymode_rope_autocomplete_key -- str. Key for the rope + " autocompletion. + call pymode#Default("g:pymode_rope_autocomplete_map", "") + " OPTION: g:pymode_rope_auto_project -- bool. Auto create ropeproject call pymode#Default("g:pymode_rope_auto_project", 1) diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 2ca6f33e..cdf77b53 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -96,8 +96,8 @@ if pymode#Option('rope') if g:pymode_rope_map_space let s:prascm = g:pymode_rope_always_show_complete_menu ? "" : "" - exe "inoremap =RopeCodeAssistInsertMode()" . s:prascm - exe "inoremap =RopeCodeAssistInsertMode()" . s:prascm + " exe "inoremap =RopeCodeAssistInsertMode()" . s:prascm + exe "inoremap " . g:pymode_rope_autocomplete_map . " =RopeCodeAssistInsertMode()" . s:prascm endif endif From 7f7317d540c1f30ab289d18aa54889bca1b3ad00 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 29 May 2013 12:00:50 +0800 Subject: [PATCH 020/582] Update pylama --- ftplugin/python/init-pymode.vim | 5 +- ftplugin/python/pymode.vim | 9 +- pylibs/pylama/__init__.py | 2 +- pylibs/pylama/hook.py | 15 +- pylibs/pylama/inirama.py | 269 ++++++++++++++++++++++++++++++++ pylibs/pylama/main.py | 212 +++++++++++++++---------- pylibs/pylama/mccabe.py | 4 +- pylibs/pylama/utils.py | 4 + pylibs/pymode/auto.py | 2 + pylibs/pymode/interface.py | 2 + pylibs/pymode/lint.py | 6 +- pylibs/pymode/queue.py | 2 + 12 files changed, 427 insertions(+), 105 deletions(-) create mode 100644 pylibs/pylama/inirama.py diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index be4524b5..59e287ea 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -191,7 +191,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope " OPTION: g:pymode_rope_autocomplete_key -- str. Key for the rope " autocompletion. - call pymode#Default("g:pymode_rope_autocomplete_map", "") + call pymode#Default("g:pymode_rope_autocomplete_map", "") " OPTION: g:pymode_rope_auto_project -- bool. Auto create ropeproject call pymode#Default("g:pymode_rope_auto_project", 1) @@ -237,9 +237,6 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope " OPTION: g:pymode_rope_short_prefix -- string. call pymode#Default("g:pymode_rope_short_prefix", "") - " OPTION: g:pymode_rope_map_space -- string. - call pymode#Default("g:pymode_rope_map_space", 1) - " OPTION: g:pymode_rope_vim_completion -- bool. call pymode#Default("g:pymode_rope_vim_completion", 1) diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index cdf77b53..9b85421e 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -94,10 +94,11 @@ if pymode#Option('rope') exe "noremap " . g:pymode_rope_short_prefix . "m :emenu Rope . " inoremap =RopeLuckyAssistInsertMode() - if g:pymode_rope_map_space - let s:prascm = g:pymode_rope_always_show_complete_menu ? "" : "" - " exe "inoremap =RopeCodeAssistInsertMode()" . s:prascm - exe "inoremap " . g:pymode_rope_autocomplete_map . " =RopeCodeAssistInsertMode()" . s:prascm + let s:prascm = g:pymode_rope_always_show_complete_menu ? "" : "" + + exe "inoremap " . g:pymode_rope_autocomplete_map . " =RopeCodeAssistInsertMode()" . s:prascm + if tolower(g:pymode_rope_autocomplete_map) == '' + exe "inoremap =RopeCodeAssistInsertMode()" . s:prascm endif endif diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 71308acd..c0426772 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,6 +1,6 @@ " pylama -- Python code audit. " -version_info = (0, 3, 2) +version_info = 0, 3, 7 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index c6884d5c..93152041 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -1,7 +1,10 @@ +from __future__ import unicode_literals, print_function, absolute_import + import sys from os import path as op, chmod from subprocess import Popen, PIPE -from .main import logger + +from .main import LOGGER try: @@ -20,7 +23,7 @@ def run(command): def git_hook(): from .main import check_files _, files_modified, _ = run("git diff-index --cached --name-only HEAD") - logger.setLevel('WARN') + LOGGER.setLevel('WARN') check_files([f for f in map(str, files_modified) if f.endswith('.py')]) @@ -36,7 +39,7 @@ def hg_hook(ui, repo, **kwargs): seen.add(file_) if file_.endswith('.py'): paths.append(file_) - logger.setLevel('WARN') + LOGGER.setLevel('WARN') check_files(paths) @@ -78,11 +81,11 @@ def install_hook(path): git = op.join(path, '.git', 'hooks') hg = op.join(path, '.hg') if op.exists(git): - install_git(git) and logger.warn('Git hook has been installed.') # nolint + install_git(git) and LOGGER.warn('Git hook has been installed.') # nolint elif op.exists(hg): - install_hg(git) and logger.warn('Mercurial hook has been installed.') # nolint + install_hg(git) and LOGGER.warn('Mercurial hook has been installed.') # nolint else: - logger.error('VCS has not found. Check your path.') + LOGGER.error('VCS has not found. Check your path.') sys.exit(1) diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py new file mode 100644 index 00000000..7887dafa --- /dev/null +++ b/pylibs/pylama/inirama.py @@ -0,0 +1,269 @@ +""" + Parse INI files. + +""" +from __future__ import unicode_literals, print_function, absolute_import + +import io +import re +import logging +from collections import MutableMapping +try: + from collections import OrderedDict +except ImportError as e: + from ordereddict import OrderedDict + + +__version__ = '0.2.9' +__project__ = 'Inirama' +__author__ = "Kirill Klenov " +__license__ = "BSD" + + +NS_LOGGER = logging.getLogger('inirama') + + +class Scanner(object): + + def __init__(self, source, ignore=None, patterns=None): + """ Init Scanner instance. + + :param patterns: List of token patterns [(token, regexp)] + :param ignore: List of ignored tokens + """ + self.reset(source) + if patterns: + self.patterns = [] + for k, r in patterns: + self.patterns.append((k, re.compile(r))) + + if ignore: + self.ignore = ignore + + def reset(self, source): + """ Reset scanner. + + :param source: Source for parsing + """ + self.tokens = [] + self.source = source + self.pos = 0 + + def scan(self): + """ Scan source and grab tokens. + """ + self.pre_scan() + + token = None + end = len(self.source) + + while self.pos < end: + + best_pat = None + best_pat_len = 0 + + # Check patterns + for p, regexp in self.patterns: + m = regexp.match(self.source, self.pos) + if m: + best_pat = p + best_pat_len = len(m.group(0)) + break + + if best_pat is None: + raise SyntaxError( + "SyntaxError[@char {0}: {1}]".format( + self.pos, "Bad token.")) + + # Ignore patterns + if best_pat in self.ignore: + self.pos += best_pat_len + continue + + # Create token + token = ( + best_pat, + self.source[self.pos:self.pos + best_pat_len], + self.pos, + self.pos + best_pat_len, + ) + + self.pos = token[-1] + self.tokens.append(token) + + def pre_scan(self): + """ Prepare source. + """ + pass + + def __repr__(self): + """ Print the last 5 tokens that have been scanned in + """ + return '" + + +class INIScanner(Scanner): + patterns = [ + ('SECTION', re.compile(r'\[[^]]+\]')), + ('IGNORE', re.compile(r'[ \r\t\n]+')), + ('COMMENT', re.compile(r'[;#].*')), + ('KEY', re.compile(r'[\w_]+\s*[:=].*'))] + + ignore = ['IGNORE'] + + def pre_scan(self): + escape_re = re.compile(r'\\\n[\t ]+') + self.source = escape_re.sub('', self.source) + + +undefined = object() + + +class Section(MutableMapping): + + def __init__(self, namespace, *args, **kwargs): + super(Section, self).__init__(*args, **kwargs) + self.namespace = namespace + self.__storage__ = dict() + + def __setitem__(self, name, value): + self.__storage__[name] = str(value) + + def __getitem__(self, name): + return self.__storage__[name] + + def __delitem__(self, name): + del self.__storage__[name] + + def __len__(self): + return len(self.__storage__) + + def __iter__(self): + return iter(self.__storage__) + + def __repr__(self): + return "<{0} {1}>".format(self.__class__.__name__, str(dict(self))) + + def iteritems(self): + for key in self.__storage__.keys(): + yield key, self[key] + + items = lambda s: list(s.iteritems()) + + +class InterpolationSection(Section): + + var_re = re.compile('{([^}]+)}') + + def get(self, name, default=None): + if name in self: + return self[name] + return default + + def __interpolate__(self, math): + try: + key = math.group(1).strip() + return self.namespace.default.get(key) or self[key] + except KeyError: + return '' + + def __getitem__(self, name): + value = super(InterpolationSection, self).__getitem__(name) + sample = undefined + while sample != value: + try: + sample, value = value, self.var_re.sub( + self.__interpolate__, value) + except RuntimeError: + message = "Interpolation failed: {0}".format(name) + NS_LOGGER.error(message) + raise ValueError(message) + return value + + +class Namespace(object): + + default_section = 'DEFAULT' + silent_read = True + section_type = Section + + def __init__(self, **default_items): + self.sections = OrderedDict() + for k, v in default_items.items(): + self[self.default_section][k] = v + + @property + def default(self): + """ Return default section or empty dict. + """ + return self.sections.get(self.default_section, dict()) + + def read(self, *files, **params): + """ Read and parse INI files. + """ + for f in files: + try: + with io.open(f, encoding='utf-8') as ff: + NS_LOGGER.info('Read from `{0}`'.format(ff.name)) + self.parse(ff.read(), **params) + except (IOError, TypeError, SyntaxError, io.UnsupportedOperation): + if not self.silent_read: + NS_LOGGER.error('Reading error `{0}`'.format(ff.name)) + raise + + def write(self, f): + """ + Write self as INI file. + + :param f: File object or path to file. + """ + if isinstance(f, str): + f = io.open(f, 'w', encoding='utf-8') + + if not hasattr(f, 'read'): + raise AttributeError("Wrong type of file: {0}".format(type(f))) + + NS_LOGGER.info('Write to `{0}`'.format(f.name)) + for section in self.sections.keys(): + f.write('[{0}]\n'.format(section)) + for k, v in self[section].items(): + f.write('{0:15}= {1}\n'.format(k, v)) + f.write('\n') + f.close() + + def parse(self, source, update=True, **params): + """ Parse INI source. + """ + scanner = INIScanner(source) + scanner.scan() + + section = self.default_section + + for token in scanner.tokens: + if token[0] == 'KEY': + name, value = re.split('[=:]', token[1], 1) + name, value = name.strip(), value.strip() + if not update and name in self[section]: + continue + self[section][name] = value + + elif token[0] == 'SECTION': + section = token[1].strip('[]') + + def __getitem__(self, name): + """ Look name in self sections. + """ + if not name in self.sections: + self.sections[name] = self.section_type(self) + return self.sections[name] + + def __repr__(self): + return "".format(self.sections) + + +class InterpolationNamespace(Namespace): + + section_type = InterpolationSection + +# lint_ignore=W0201,R0924,F0401 diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index aebad2d4..20f68218 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -1,50 +1,55 @@ +from __future__ import ( + unicode_literals, print_function, absolute_import, with_statement +) + import fnmatch +import logging import re import sys -from os import getcwd, walk, path as op - -import logging from argparse import ArgumentParser +from os import getcwd, walk, path as op -from . import utils +from . import utils, version +from .inirama import Namespace -default_linters = 'pep8', 'pyflakes', 'mccabe' -default_complexity = 10 -logger = logging.Logger('pylama') -stream = logging.StreamHandler() -logger.addHandler(stream) +DEFAULT_LINTERS = 'pep8', 'pyflakes', 'mccabe' +DEFAULT_COMPLEXITY = 10 +LOGGER = logging.Logger('pylama') +STREAM = logging.StreamHandler() +LOGGER.addHandler(STREAM) SKIP_PATTERN = '# nolint' -def run(path, ignore=None, select=None, linters=default_linters, **meta): # nolint +def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, + **meta): errors = [] ignore = ignore and list(ignore) or [] select = select and list(select) or [] - for lint in linters: - try: - linter = getattr(utils, lint) - except AttributeError: - logging.warning("Linter `{0}` not found.".format(lint)) - continue + try: + with open(path, 'rU') as f: + code = f.read() + '\n\n' + params = config or parse_modeline(code) + params['skip'] = [False] + for line in code.split('\n'): + params['skip'].append(line.endswith(SKIP_PATTERN)) - try: - with open(path, "rU") as f: - code = f.read() + '\n\n' - params = parse_modeline(code) - params['skip'] = [False] - for line in code.split('\n'): - params['skip'].append(line.endswith(SKIP_PATTERN)) + if params.get('lint_ignore'): + ignore += params.get('lint_ignore').split(',') - if params.get('lint_ignore'): - ignore += params.get('lint_ignore').split(',') + if params.get('lint_select'): + select += params.get('lint_select').split(',') - if params.get('lint_select'): - select += params.get('lint_select').split(',') + if params.get('lint'): + for lint in linters: + try: + linter = getattr(utils, lint) + except AttributeError: + logging.warning("Linter `{0}` not found.".format(lint)) + continue - if params.get('lint'): result = linter(path, code=code, **meta) for e in result: e['col'] = e.get('col') or 0 @@ -57,27 +62,27 @@ def run(path, ignore=None, select=None, linters=default_linters, **meta): # nol if not params['skip'][e['lnum']]: errors.append(e) - except IOError as e: - errors.append(dict( - lnum=0, - type='E', - col=0, - text=str(e) - )) - except SyntaxError as e: - errors.append(dict( - lnum=e.lineno or 0, - type='E', - col=e.offset or 0, - text=e.args[0] - )) - break - - except Exception as e: - import traceback - logging.error(traceback.format_exc()) - - errors = [e for e in errors if _ignore_error(e, select, ignore)] + except IOError as e: + errors.append(dict( + lnum=0, + type='E', + col=0, + text=str(e) + )) + + except SyntaxError as e: + errors.append(dict( + lnum=e.lineno or 0, + type='E', + col=e.offset or 0, + text=e.args[0] + )) + + except Exception: + import traceback + logging.error(traceback.format_exc()) + + errors = [er for er in errors if _ignore_error(er, select, ignore)] return sorted(errors, key=lambda x: x['lnum']) @@ -92,11 +97,14 @@ def _ignore_error(e, select, ignore): def shell(): + curdir = getcwd() parser = ArgumentParser(description="Code audit tool for python.") - parser.add_argument("path", nargs='?', default=getcwd(), + parser.add_argument("path", nargs='?', default=curdir, help="Path on file or directory.") parser.add_argument( "--verbose", "-v", action='store_true', help="Verbose mode.") + parser.add_argument('--version', action='version', + version='%(prog)s ' + version) split_csp_list = lambda s: list(set(i for i in s.split(',') if i)) @@ -108,7 +116,7 @@ def shell(): type=split_csp_list, help="Select errors and warnings. (comma-separated)") parser.add_argument( - "--linters", "-l", default=','.join(default_linters), + "--linters", "-l", default=','.join(DEFAULT_LINTERS), type=split_csp_list, help="Select linters. (comma-separated)") parser.add_argument( @@ -120,61 +128,92 @@ def shell(): type=lambda s: [re.compile(fnmatch.translate(p)) for p in s.split(',')], help="Skip files by masks (comma-separated, Ex. */messages.py)") - parser.add_argument("--complexity", "-c", default=default_complexity, + parser.add_argument("--complexity", "-c", default=DEFAULT_COMPLEXITY, type=int, help="Set mccabe complexity.") parser.add_argument("--report", "-r", help="Filename for report.") parser.add_argument("--hook", action="store_true", help="Install Git (Mercurial) hook.") - args = parser.parse_args() - - # Setup logger - logger.setLevel(logging.INFO if args.verbose else logging.WARN) - if args.report: - logger.removeHandler(stream) - logger.addHandler(logging.FileHandler(args.report, mode='w')) - - if args.hook: + parser.add_argument( + "--options", "-o", default=op.join(curdir, 'pylama.ini'), + help="Select configuration file. By default is '/pylama.ini'") + options = parser.parse_args() + actions = dict((a.dest, a) for a in parser._actions) + + # Setup LOGGER + LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) + if options.report: + LOGGER.removeHandler(STREAM) + LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) + + # Read options from configuration file + config = Namespace() + config.default_section = 'main' + LOGGER.info('Try to read configuration from: ' + options.options) + config.read(options.options) + for k, v in config.default.items(): + action = actions.get(k) + if action: + LOGGER.info('Find option %s (%s)' % (k, v)) + name, value = action.dest, action.type(v)\ + if callable(action.type) else v + setattr(options, name, value) + + # Install VSC hook + if options.hook: from .hook import install_hook - return install_hook(args.path) + return install_hook(options.path) - paths = [args.path] + paths = [options.path] - if op.isdir(args.path): + if op.isdir(options.path): paths = [] - for root, _, files in walk(args.path): + for root, _, files in walk(options.path): paths += [op.join(root, f) for f in files if f.endswith('.py')] check_files( paths, - rootpath=args.path, - skip=args.skip, - frmt=args.format, - ignore=args.ignore, - select=args.select, - linters=args.linters, - complexity=args.complexity, + rootpath=options.path, + skip=options.skip, + frmt=options.format, + ignore=options.ignore, + select=options.select, + linters=options.linters, + complexity=options.complexity, + config=config, ) def check_files(paths, rootpath=None, skip=None, frmt="pep8", - select=None, ignore=None, linters=default_linters, - complexity=default_complexity): + select=None, ignore=None, linters=DEFAULT_LINTERS, + complexity=DEFAULT_COMPLEXITY, config=None): rootpath = rootpath or getcwd() pattern = "%(rel)s:%(lnum)s:%(col)s: %(text)s" if frmt == 'pylint': pattern = "%(rel)s:%(lnum)s: [%(type)s] %(text)s" + params = dict() + if config: + for key, section in config.sections.items(): + if key != 'main': + params[op.abspath(key)] = prepare_params(section) + errors = [] - for path in skip_paths(skip, paths): - logger.info("Parse file: %s" % path) - errors = run(path, ignore=ignore, select=select, - linters=linters, complexity=complexity) + + for path in paths: + path = op.abspath(path) + if any(pattern.match(path) for pattern in skip): + LOGGER.info('Skip path: %s' % path) + continue + + LOGGER.info("Parse file: %s" % path) + errors = run(path, ignore=ignore, select=select, linters=linters, + complexity=complexity, config=params.get(path)) for error in errors: try: error['rel'] = op.relpath( error['filename'], op.dirname(rootpath)) error['col'] = error.get('col', 1) - logger.warning(pattern, error) + LOGGER.warning(pattern, error) except KeyError: continue @@ -185,13 +224,6 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", r'^\s*#\s+(?:pymode\:)?((?:lint[\w_]*=[^:\n\s]+:?)+)', re.I | re.M) -def skip_paths(skip, paths): - for path in paths: - if skip and any(pattern.match(path) for pattern in skip): - continue - yield path - - def parse_modeline(code): seek = MODERE.search(code) params = dict(lint=1) @@ -201,5 +233,13 @@ def parse_modeline(code): return params +def prepare_params(section): + params = dict(section) + params['lint'] = int(params.get('lint', 1)) + return params + + if __name__ == '__main__': shell() + +# lint_ignore=R0914,C901,W0212 diff --git a/pylibs/pylama/mccabe.py b/pylibs/pylama/mccabe.py index 432f4c1e..c95f561d 100644 --- a/pylibs/pylama/mccabe.py +++ b/pylibs/pylama/mccabe.py @@ -3,7 +3,9 @@ http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html MIT License. """ -from __future__ import with_statement +from __future__ import ( + unicode_literals, print_function, absolute_import, with_statement +) import sys diff --git a/pylibs/pylama/utils.py b/pylibs/pylama/utils.py index ec98e4e2..51ab1d4d 100644 --- a/pylibs/pylama/utils.py +++ b/pylibs/pylama/utils.py @@ -1,3 +1,7 @@ +from __future__ import ( + unicode_literals, print_function, absolute_import, with_statement +) + import _ast from os import path as op, environ diff --git a/pylibs/pymode/auto.py b/pylibs/pymode/auto.py index 16d7b811..f353b832 100644 --- a/pylibs/pymode/auto.py +++ b/pylibs/pymode/auto.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import vim from autopep8 import fix_file diff --git a/pylibs/pymode/interface.py b/pylibs/pymode/interface.py index 3134882d..8e70d331 100644 --- a/pylibs/pymode/interface.py +++ b/pylibs/pymode/interface.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import vim diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index 6746332f..dbc164e6 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import locale from pylama.main import run @@ -47,12 +49,10 @@ def run_checkers(checkers=None, ignore=None, buf=None, select=None, complexity=None, callback=None): filename = buf.name - result = [] - pylint_options = '--rcfile={0} -r n'.format(get_var('lint_config')).split() return run(filename, ignore=ignore, select=select, linters=checkers, - pylint=pylint_options, complexity=complexity) + pylint=pylint_options, complexity=complexity) def parse_result(result, buf=None, **kwargs): diff --git a/pylibs/pymode/queue.py b/pylibs/pymode/queue.py index d1d087ae..5aa5c26d 100644 --- a/pylibs/pymode/queue.py +++ b/pylibs/pymode/queue.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import threading from Queue import Queue, Empty From b845ed0a11ac49ed8cf7f8800548b87f97fd0575 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 29 May 2013 12:04:47 +0800 Subject: [PATCH 021/582] Update Changelog --- Changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Changelog.rst b/Changelog.rst index e3d57589..f6fdb811 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,6 +1,9 @@ Changelog ========= +* Added `g:pymode_rope_autocomplete_map` option; +* Removed `g:pymode_rope_map_space` option; + ## 2013-05-15 0.6.18 -------------------- * Fixed autopep8 (`PyLintAuto`) command; From 02f5ae248d75b17b4993be55fd0a9aaa027890c4 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 29 May 2013 12:06:26 +0800 Subject: [PATCH 022/582] Added a simple logo --- README.rst | 5 +++-- logo.png | Bin 0 -> 62060 bytes 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 logo.png diff --git a/README.rst b/README.rst index 8d7a58c9..deb602c5 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Python-mode, Python in VIM -########################## +|logo| Python-mode, Python in VIM +################################# Python-mode is a vim plugin that allows you to use the pylint_, rope_, pydoc_, pyflakes_, pep8_, mccabe_ libraries in vim to provide features like python code looking for bugs, refactoring and some other useful things. @@ -524,3 +524,4 @@ My address is here: "Russia, 143401, Krasnogorsk, Shkolnaya 1-19" to "Kirill Kle .. _pathogen: https://github.com/tpope/vim-pathogen .. _pep8: http://pypi.python.org/pypi/pep8 .. _mccabe: http://en.wikipedia.org/wiki/Cyclomatic_complexity +.. |logo| image:: https://raw.github.com/klen/python-mode/develop/logo.png diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..908cc2250133602cba3058cd8742c1f9e2366a7a GIT binary patch literal 62060 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¨G+hcBD571pOalIsGwZj*3SjuAXfd;0K-7R6W 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�OmmOZRZ%b9DVA3s*V>A0(`R`k=c#={WVs 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 literal 0 HcmV?d00001 From 8d5c556aa8fdb3e177abf1a61587d4cc1a1668fb Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Wed, 29 May 2013 14:46:41 +0800 Subject: [PATCH 023/582] Change logo size --- logo.png | Bin 62060 -> 16429 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/logo.png b/logo.png index 908cc2250133602cba3058cd8742c1f9e2366a7a..b873f5cbeecd98729f2c9759dc43f975640fdf70 100644 GIT binary patch literal 16429 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¨G+hcBD571pOalIsGwZj*3SjuAXfd;0K-7R6W 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�OmmOZRZ%b9DVA3s*V>A0(`R`k=c#={WVs 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 From bc5dc3db8ba2ff53fbcc3a6db89f621fec3b7a3c Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 31 May 2013 00:41:42 +0800 Subject: [PATCH 024/582] Fix checking --- autoload/pymode/lint.vim | 1 - autoload/pymode/queue.vim | 8 +++++--- pylibs/pymode/lint.py | 4 ++-- pylibs/pymode/queue.py | 8 +++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/autoload/pymode/lint.vim b/autoload/pymode/lint.vim index 082f80b8..68782e18 100644 --- a/autoload/pymode/lint.vim +++ b/autoload/pymode/lint.vim @@ -15,7 +15,6 @@ fun! pymode#lint#Check() "{{{ let g:pymode_lint_buffer = bufnr('%') py from pymode import lint - py queue.stop_queue(False) py lint.check_file() endfunction " }}} diff --git a/autoload/pymode/queue.vim b/autoload/pymode/queue.vim index 8de52cdb..b3160ee0 100644 --- a/autoload/pymode/queue.vim +++ b/autoload/pymode/queue.vim @@ -5,9 +5,11 @@ fun! pymode#queue#Poll() "{{{ " Update interval if mode() == 'i' - let p = getpos('.') - silent exe 'call feedkeys("\\", "n")' - call setpos('.', p) + if col('.') == 1 + call feedkeys("\\", "n") + else + call feedkeys("\\", "n") + endif else call feedkeys("f\e", "n") endif diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index dbc164e6..4c28c25c 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import locale +import json from pylama.main import run @@ -56,8 +57,7 @@ def run_checkers(checkers=None, ignore=None, buf=None, select=None, def parse_result(result, buf=None, **kwargs): - command(('let g:qf_list = {0}'.format(repr(result)).replace( - '\': u', '\': '))) + command('let g:qf_list = ' + json.dumps(result)) command('call pymode#lint#Parse({0})'.format(buf.number)) # pymode:lint_ignore=W0622 diff --git a/pylibs/pymode/queue.py b/pylibs/pymode/queue.py index 5aa5c26d..e43de9c8 100644 --- a/pylibs/pymode/queue.py +++ b/pylibs/pymode/queue.py @@ -9,13 +9,14 @@ MAX_LIFE = 60 CHECK_INTERVAL = .2 RESULTS = Queue() +TEST = 1 class Task(threading.Thread): def __init__(self, *args, **kwargs): - self.stop = threading.Event() threading.Thread.__init__(self, *args, **kwargs) + self.stop = threading.Event() def run(self): """ Run the task. @@ -34,6 +35,11 @@ def run(self): def add_task(target, title=None, *args, **kwargs): " Add all tasks. " + # Only one task at time + for thread in threading.enumerate(): + if isinstance(thread, Task): + return True + task = Task(target=target, args=args, kwargs=kwargs) task.daemon = True task.start() From f98662e64525e5e729f931d88adf856b14133fee Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 31 May 2013 10:39:25 +0800 Subject: [PATCH 025/582] Fix troubleshooting function --- autoload/pymode/troubleshooting.vim | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/autoload/pymode/troubleshooting.vim b/autoload/pymode/troubleshooting.vim index 02e00a26..dca1ea7d 100644 --- a/autoload/pymode/troubleshooting.vim +++ b/autoload/pymode/troubleshooting.vim @@ -1,5 +1,7 @@ " DESC: Get debug information about pymode problem fun! pymode#troubleshooting#Test() "{{{ + runtime ftplugin/python/init-pymode.vim + new setlocal buftype=nofile bufhidden=delete noswapfile nowrap @@ -33,7 +35,9 @@ fun! pymode#troubleshooting#Test() "{{{ call append('$', 'let pymode = ' . string(g:pymode)) if g:pymode call append('$', 'let pymode_path = ' . string(g:pymode_path)) - call append('$', 'let pymode_paths = ' . string(g:pymode_paths)) + if g:pymode_path + call append('$', 'let pymode_paths = ' . string(g:pymode_paths)) + end call append('$', 'let pymode_doc = ' . string(g:pymode_doc)) if g:pymode_doc @@ -63,6 +67,12 @@ fun! pymode#troubleshooting#Test() "{{{ endif call append('$', 'let pymode_rope = ' . string(g:pymode_rope)) + if g:pymode_rope + call append('$', 'let pymode_rope_autocomplete_map = ' . string(g:pymode_rope_autocomplete_map)) + call append('$', 'let pymode_rope_auto_project = ' . string(g:pymode_rope_auto_project)) + call append('$', 'let pymode_rope_auto_project_open = ' . string(g:pymode_rope_auto_project_open)) + call append('$', 'let pymode_rope_auto_session_manage = ' . string(g:pymode_rope_auto_session_manage)) + end call append('$', 'let pymode_folding = ' . string(g:pymode_folding)) call append('$', 'let pymode_breakpoint = ' . string(g:pymode_breakpoint)) call append('$', 'let pymode_syntax = ' . string(g:pymode_syntax)) @@ -75,6 +85,7 @@ fun! pymode#troubleshooting#Test() "{{{ endif if python + call append('$', '') call append('$', 'VIM python paths:') call append('$', '-----------------') python << EOF From 7bff0c2c9515b07aab29432bae3cbd664bd29918 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 31 May 2013 18:09:24 +0800 Subject: [PATCH 026/582] Update pylama --- pylibs/pylama/__init__.py | 16 ++++++++++++++-- pylibs/pylama/hook.py | 2 +- pylibs/pylama/inirama.py | 2 +- pylibs/pylama/main.py | 2 +- pylibs/pylama/mccabe.py | 2 +- pylibs/pylama/utils.py | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index c0426772..bcdf2ebc 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,6 +1,18 @@ -" pylama -- Python code audit. " +""" + Code audit tool for python. Pylama wraps these tools: -version_info = 0, 3, 7 + * PEP8_ (c) 2012-2013, Florent Xicluna; + * PyFlakes_ (c) 2005-2013, Kevin Watters; + * Pylint_ (c) 2013, Logilab; + * Mccabe_ (c) Ned Batchelder; + + | `Pylint doesnt supported in python3.` + + :copyright: 2013 by Kirill Klenov. + :license: BSD, see LICENSE for more details. +""" + +version_info = 1, 0, 1 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index 93152041..c2a5d80b 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals, print_function, absolute_import +from __future__ import print_function, absolute_import import sys from os import path as op, chmod diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index 7887dafa..da770227 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -2,7 +2,7 @@ Parse INI files. """ -from __future__ import unicode_literals, print_function, absolute_import +from __future__ import print_function, absolute_import import io import re diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index 20f68218..409099cb 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -1,5 +1,5 @@ from __future__ import ( - unicode_literals, print_function, absolute_import, with_statement + print_function, absolute_import, with_statement ) import fnmatch diff --git a/pylibs/pylama/mccabe.py b/pylibs/pylama/mccabe.py index c95f561d..19931dbe 100644 --- a/pylibs/pylama/mccabe.py +++ b/pylibs/pylama/mccabe.py @@ -4,7 +4,7 @@ MIT License. """ from __future__ import ( - unicode_literals, print_function, absolute_import, with_statement + print_function, absolute_import, with_statement ) import sys diff --git a/pylibs/pylama/utils.py b/pylibs/pylama/utils.py index 51ab1d4d..d8f6febc 100644 --- a/pylibs/pylama/utils.py +++ b/pylibs/pylama/utils.py @@ -1,5 +1,5 @@ from __future__ import ( - unicode_literals, print_function, absolute_import, with_statement + print_function, absolute_import, with_statement ) import _ast From 4648395d7ba694473d14d95e954fa9d8dfcf893f Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 31 May 2013 18:27:50 +0800 Subject: [PATCH 027/582] update pylama --- pylibs/pylama/__init__.py | 2 +- pylibs/pylama/hook.py | 2 +- pylibs/pylama/inirama.py | 2 +- pylibs/pylama/main.py | 6 ++---- pylibs/pylama/mccabe.py | 4 +--- pylibs/pylama/utils.py | 6 ++---- 6 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index bcdf2ebc..2be0a99b 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -12,7 +12,7 @@ :license: BSD, see LICENSE for more details. """ -version_info = 1, 0, 1 +version_info = 1, 0, 2 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index c2a5d80b..36fa211c 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -1,4 +1,4 @@ -from __future__ import print_function, absolute_import +from __future__ import absolute_import import sys from os import path as op, chmod diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index da770227..42cf2389 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -2,7 +2,7 @@ Parse INI files. """ -from __future__ import print_function, absolute_import +from __future__ import absolute_import import io import re diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index 409099cb..92c3e455 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -1,6 +1,4 @@ -from __future__ import ( - print_function, absolute_import, with_statement -) +from __future__ import absolute_import, with_statement import fnmatch import logging @@ -75,7 +73,7 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, lnum=e.lineno or 0, type='E', col=e.offset or 0, - text=e.args[0] + text=e.args[0] + ' [%s]' % lint )) except Exception: diff --git a/pylibs/pylama/mccabe.py b/pylibs/pylama/mccabe.py index 19931dbe..71f024a9 100644 --- a/pylibs/pylama/mccabe.py +++ b/pylibs/pylama/mccabe.py @@ -3,9 +3,7 @@ http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html MIT License. """ -from __future__ import ( - print_function, absolute_import, with_statement -) +from __future__ import absolute_import, with_statement import sys diff --git a/pylibs/pylama/utils.py b/pylibs/pylama/utils.py index d8f6febc..78460ce4 100644 --- a/pylibs/pylama/utils.py +++ b/pylibs/pylama/utils.py @@ -1,6 +1,4 @@ -from __future__ import ( - print_function, absolute_import, with_statement -) +from __future__ import absolute_import, with_statement import _ast from os import path as op, environ @@ -52,7 +50,7 @@ def pep8(path, **meta): def mccabe(path, code=None, complexity=8, **meta): " MCCabe code checking. " - return get_code_complexity(code, complexity, filename=path) + return get_code_complexity(code, complexity, filename=path) or [] def pyflakes(path, code=None, **meta): From 77d60d53b9425fc82edfcb294e1948c7c22c7c2c Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 6 Jun 2013 16:52:59 +0800 Subject: [PATCH 028/582] Lint respects pylama.ini and pymode.ini files. --- pylibs/pylama/inirama.py | 3 ++ pylibs/pymode/interface.py | 11 +++++-- pylibs/pymode/lint.py | 62 ++++++++++++++++++++++++-------------- 3 files changed, 51 insertions(+), 25 deletions(-) diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index 42cf2389..8226c139 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -258,6 +258,9 @@ def __getitem__(self, name): self.sections[name] = self.section_type(self) return self.sections[name] + def __contains__(self, name): + return name in self.sections + def __repr__(self): return "".format(self.sections) diff --git a/pylibs/pymode/interface.py b/pylibs/pymode/interface.py index 8e70d331..fe99d540 100644 --- a/pylibs/pymode/interface.py +++ b/pylibs/pymode/interface.py @@ -12,7 +12,8 @@ def get_var(name): def get_bvar(name): - return (int(vim.eval("exists('b:pymode_%s')" % name)) and vim.eval("b:pymode_%s" % name)) or None + return (int(vim.eval("exists('b:pymode_%s')" % name)) + and vim.eval("b:pymode_%s" % name)) or None def get_current_buffer(): @@ -24,4 +25,10 @@ def show_message(message): def command(cmd): - vim.command(cmd) + return vim.command(cmd) + + +def eval_code(code): + return vim.eval(code) + +# lint_ignore=F0401 diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index 4c28c25c..51930bce 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -3,9 +3,11 @@ import locale import json -from pylama.main import run +from pylama.main import run, prepare_params +from pylama.inirama import Namespace +from os import path as op -from .interface import get_option, get_var, get_current_buffer, command +from . import interface from .queue import add_task @@ -16,48 +18,62 @@ def check_file(): - checkers = get_option('lint_checker').split(',') + checkers = interface.get_option('lint_checker').split(',') + buf = interface.get_current_buffer() + + # Check configuration from `pymode.ini` + curdir = interface.eval_code('getcwd()') + config = Namespace() + config.default_section = 'main' + config.read(op.join(curdir, 'pylama.ini'), op.join(curdir, 'pymode.ini')) ignore = set([ i for i in ( - get_option('lint_ignore').split(',') + - get_var('lint_ignore').split(',')) - if i + interface.get_option('lint_ignore').split(',') + + interface.get_var('lint_ignore').split(',') + + config.default.get('ignore', '').split(',') + ) if i ]) + select = set([ s for s in ( - get_option('lint_select').split(',') + - get_var('lint_select').split(',')) - if s + interface.get_option('lint_select').split(',') + + interface.get_var('lint_select').split(',') + + config.default.get('select', '').split(',') + ) if s ]) - buf = get_current_buffer() - complexity = int(get_option('lint_mccabe_complexity') or 0) + complexity = int(interface.get_option('lint_mccabe_complexity') or 0) + + params = None + relpath = op.relpath(buf.name, curdir) + if relpath in config: + params = prepare_params(config[relpath]) add_task( run_checkers, callback=parse_result, title='Code checking', - checkers=checkers, - ignore=ignore, - buf=buf, - select=select, - complexity=complexity) + # params + checkers=checkers, ignore=ignore, buf=buf, select=select, + complexity=complexity, config=params, + ) def run_checkers(checkers=None, ignore=None, buf=None, select=None, - complexity=None, callback=None): + complexity=None, callback=None, config=None): - filename = buf.name - pylint_options = '--rcfile={0} -r n'.format(get_var('lint_config')).split() + pylint_options = '--rcfile={0} -r n'.format( + interface.get_var('lint_config')).split() - return run(filename, ignore=ignore, select=select, linters=checkers, - pylint=pylint_options, complexity=complexity) + return run( + buf.name, ignore=ignore, select=select, linters=checkers, + pylint=pylint_options, complexity=complexity, config=config) def parse_result(result, buf=None, **kwargs): - command('let g:qf_list = ' + json.dumps(result)) - command('call pymode#lint#Parse({0})'.format(buf.number)) + interface.command('let g:qf_list = ' + json.dumps(result)) + interface.command('call pymode#lint#Parse({0})'.format(buf.number)) # pymode:lint_ignore=W0622 From 2b7c208c9c6f9198c8fb16d510e5a2555422f286 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 7 Jun 2013 11:00:22 +0800 Subject: [PATCH 029/582] Update pylama --- pylibs/pylama/__init__.py | 11 +- pylibs/pylama/core.py | 112 +++++++ pylibs/pylama/hook.py | 18 +- pylibs/pylama/inirama.py | 68 +++- pylibs/pylama/main.py | 203 ++++-------- pylibs/pylama/pep257.py | 676 ++++++++++++++++++++++++++++++++++++++ pylibs/pylama/tasks.py | 105 ++++++ pylibs/pylama/utils.py | 71 +++- pylibs/pymode/lint.py | 3 +- 9 files changed, 1098 insertions(+), 169 deletions(-) create mode 100644 pylibs/pylama/core.py create mode 100644 pylibs/pylama/pep257.py create mode 100644 pylibs/pylama/tasks.py diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 2be0a99b..6d528b30 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,18 +1,11 @@ """ - Code audit tool for python. Pylama wraps these tools: - - * PEP8_ (c) 2012-2013, Florent Xicluna; - * PyFlakes_ (c) 2005-2013, Kevin Watters; - * Pylint_ (c) 2013, Logilab; - * Mccabe_ (c) Ned Batchelder; - - | `Pylint doesnt supported in python3.` + Code audit tool for python. :copyright: 2013 by Kirill Klenov. :license: BSD, see LICENSE for more details. """ -version_info = 1, 0, 2 +version_info = 1, 0, 4 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/core.py b/pylibs/pylama/core.py new file mode 100644 index 00000000..70915075 --- /dev/null +++ b/pylibs/pylama/core.py @@ -0,0 +1,112 @@ +""" Pylama core. +""" +import logging +import re + +from . import utils + + +DEFAULT_LINTERS = 'pep8', 'pyflakes', 'mccabe' +LOGGER = logging.getLogger('pylama') +MODERE = re.compile(r'^\s*#\s+(?:pymode\:)?((?:lint[\w_]*=[^:\n\s]+:?)+)', + re.I | re.M) +SKIP_PATTERN = '# nolint' +STREAM = logging.StreamHandler() + +LOGGER.addHandler(STREAM) + + +def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, + **meta): + """ Run code checking for path. + + :return errors: list of dictionaries with error's information + + """ + errors = [] + ignore = ignore and list(ignore) or [] + select = select and list(select) or [] + + try: + with open(path, 'rU') as f: + code = f.read() + '\n\n' + params = config or __parse_modeline(code) + params['skip'] = [False] + for line in code.split('\n'): + params['skip'].append(line.endswith(SKIP_PATTERN)) + + if params.get('lint_ignore'): + ignore += params.get('lint_ignore').split(',') + + if params.get('lint_select'): + select += params.get('lint_select').split(',') + + if params.get('lint'): + for lint in linters: + try: + linter = getattr(utils, lint) + except AttributeError: + LOGGER.warning("Linter `%s` not found.", lint) + continue + + result = linter(path, code=code, **meta) + for e in result: + e['col'] = e.get('col') or 0 + e['lnum'] = e.get('lnum') or 0 + e['type'] = e.get('type') or 'E' + e['text'] = "{0} [{1}]".format((e.get( + 'text') or '').strip() + .replace("'", "\"").split('\n')[0], lint) + e['filename'] = path or '' + try: + if not params['skip'][e['lnum']]: + errors.append(e) + except IndexError: + continue + + except IOError as e: + errors.append(dict( + lnum=0, + type='E', + col=0, + text=str(e) + )) + + except SyntaxError as e: + errors.append(dict( + lnum=e.lineno or 0, + type='E', + col=e.offset or 0, + text=e.args[0] + ' [%s]' % lint + )) + + except Exception: + import traceback + logging.error(traceback.format_exc()) + + errors = [er for er in errors if __ignore_error(er, select, ignore)] + return sorted(errors, key=lambda x: x['lnum']) + + +def __parse_modeline(code): + """ Parse modeline params from file. + + :return dict: Linter params. + + """ + seek = MODERE.search(code) + params = dict(lint=1) + if seek: + params = dict(v.split('=') for v in seek.group(1).split(':')) + params['lint'] = int(params.get('lint', 1)) + return params + + +def __ignore_error(e, select, ignore): + for s in select: + if e['text'].startswith(s): + return True + for i in ignore: + if e['text'].startswith(i): + return False + return True diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index 36fa211c..fa50899c 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -1,3 +1,5 @@ +""" SCM hooks. +""" from __future__ import absolute_import import sys @@ -14,6 +16,11 @@ def run(command): + """ Run a shell command. + + :return str: Stdout + + """ p = Popen(command.split(), stdout=PIPE, stderr=PIPE) (stdout, stderr) = p.communicate() return (p.returncode, [line.strip() for line in stdout.splitlines()], @@ -21,6 +28,8 @@ def run(command): def git_hook(): + """ Run pylama after git commit. """ + from .main import check_files _, files_modified, _ = run("git diff-index --cached --name-only HEAD") LOGGER.setLevel('WARN') @@ -28,6 +37,8 @@ def git_hook(): def hg_hook(ui, repo, **kwargs): + """ Run pylama after mercurial commit. """ + from .main import check_files seen = set() paths = [] @@ -44,6 +55,7 @@ def hg_hook(ui, repo, **kwargs): def install_git(path): + """ Install hook in Git repository. """ hook = op.join(path, 'pre-commit') with open(hook, 'w+') as fd: fd.write("""#!/usr/bin/env python @@ -54,10 +66,11 @@ def install_git(path): sys.exit(git_hook()) """) chmod(hook, 484) - return True def install_hg(path): + """ Install hook in Mercurial repository. """ + hook = op.join(path, 'hgrc') if not op.isfile(hook): open(hook, 'w+').close() @@ -74,10 +87,11 @@ def install_hg(path): c.set('hooks', 'qrefresh', 'python:pylama.hooks.hg_hook') c.write(open(path, 'w+')) - return True def install_hook(path): + """ Auto definition of SCM and hook installation. """ + git = op.join(path, '.git', 'hooks') hg = op.join(path, '.hg') if op.exists(git): diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index 8226c139..22998b2b 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -1,8 +1,14 @@ """ - Parse INI files. + Inirama is a python module that parses INI files. + .. include:: ../README.rst + :start-line: 5 + :end-line: 12 + + :copyright: 2013 by Kirill Klenov. + :license: BSD, see LICENSE for more details. """ -from __future__ import absolute_import +from __future__ import unicode_literals, print_function import io import re @@ -10,11 +16,11 @@ from collections import MutableMapping try: from collections import OrderedDict -except ImportError as e: +except ImportError: from ordereddict import OrderedDict -__version__ = '0.2.9' +__version__ = '0.4.0' __project__ = 'Inirama' __author__ = "Kirill Klenov " __license__ = "BSD" @@ -183,9 +189,33 @@ def __getitem__(self, name): class Namespace(object): + """ Default class for parsing INI. + + :param **default_items: Default items for default section. + + Usage + ----- + + :: + + from inirama import Namespace + ns = Namespace() + ns.read('config.ini') + + print ns['section']['key'] + + ns['other']['new'] = 'value' + ns.write('new_config.ini') + + """ + #: Name of default section (:attr:`~inirama.Namespace.default`) default_section = 'DEFAULT' + + #: Dont raise any exception on file reading erorrs silent_read = True + + #: Class for generating sections section_type = Section def __init__(self, **default_items): @@ -201,6 +231,11 @@ def default(self): def read(self, *files, **params): """ Read and parse INI files. + + :param *files: Files for reading + :param **params: Params for parsing + + Set `update=False` for prevent values redefinition. """ for f in files: try: @@ -214,7 +249,7 @@ def read(self, *files, **params): def write(self, f): """ - Write self as INI file. + Write namespace as INI file. :param f: File object or path to file. """ @@ -233,7 +268,10 @@ def write(self, f): f.close() def parse(self, source, update=True, **params): - """ Parse INI source. + """ Parse INI source as string. + + :param source: Source of INI + :param update: Replace alredy defined items """ scanner = INIScanner(source) scanner.scan() @@ -266,7 +304,23 @@ def __repr__(self): class InterpolationNamespace(Namespace): + """ That implements the interpolation feature. + + :: + + from inirama import InterpolationNamespace + + ns = InterpolationNamespace() + ns.parse(''' + [main] + test = value + foo = bar {test} + more_deep = wow {foo} + ''') + print ns['main']['more_deep'] # wow bar value + + """ section_type = InterpolationSection -# lint_ignore=W0201,R0924,F0401 +# lint_ignore=W0201,R0924 diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index 92c3e455..a9cf9473 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -1,3 +1,5 @@ +""" Pylama shell integration. +""" from __future__ import absolute_import, with_statement import fnmatch @@ -8,93 +10,14 @@ from os import getcwd, walk, path as op from . import utils, version -from .inirama import Namespace +from .core import DEFAULT_LINTERS, LOGGER, STREAM -DEFAULT_LINTERS = 'pep8', 'pyflakes', 'mccabe' DEFAULT_COMPLEXITY = 10 -LOGGER = logging.Logger('pylama') -STREAM = logging.StreamHandler() -LOGGER.addHandler(STREAM) - -SKIP_PATTERN = '# nolint' - - -def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, - **meta): - errors = [] - ignore = ignore and list(ignore) or [] - select = select and list(select) or [] - - try: - with open(path, 'rU') as f: - code = f.read() + '\n\n' - params = config or parse_modeline(code) - params['skip'] = [False] - for line in code.split('\n'): - params['skip'].append(line.endswith(SKIP_PATTERN)) - - if params.get('lint_ignore'): - ignore += params.get('lint_ignore').split(',') - - if params.get('lint_select'): - select += params.get('lint_select').split(',') - - if params.get('lint'): - for lint in linters: - try: - linter = getattr(utils, lint) - except AttributeError: - logging.warning("Linter `{0}` not found.".format(lint)) - continue - - result = linter(path, code=code, **meta) - for e in result: - e['col'] = e.get('col') or 0 - e['lnum'] = e.get('lnum') or 0 - e['type'] = e.get('type') or 'E' - e['text'] = "{0} [{1}]".format((e.get( - 'text') or '').strip() - .replace("'", "\"").split('\n')[0], lint) - e['filename'] = path or '' - if not params['skip'][e['lnum']]: - errors.append(e) - - except IOError as e: - errors.append(dict( - lnum=0, - type='E', - col=0, - text=str(e) - )) - - except SyntaxError as e: - errors.append(dict( - lnum=e.lineno or 0, - type='E', - col=e.offset or 0, - text=e.args[0] + ' [%s]' % lint - )) - - except Exception: - import traceback - logging.error(traceback.format_exc()) - - errors = [er for er in errors if _ignore_error(er, select, ignore)] - return sorted(errors, key=lambda x: x['lnum']) - - -def _ignore_error(e, select, ignore): - for s in select: - if e['text'].startswith(s): - return True - for i in ignore: - if e['text'].startswith(i): - return False - return True def shell(): + """ Endpoint for console. """ curdir = getcwd() parser = ArgumentParser(description="Code audit tool for python.") parser.add_argument("path", nargs='?', default=curdir, @@ -116,7 +39,10 @@ def shell(): parser.add_argument( "--linters", "-l", default=','.join(DEFAULT_LINTERS), type=split_csp_list, - help="Select linters. (comma-separated)") + help=( + "Select linters. (comma-separated). Choices are %s." + % ','.join(s for s in utils.__all__) + )) parser.add_argument( "--ignore", "-i", default='', type=split_csp_list, @@ -131,59 +57,73 @@ def shell(): parser.add_argument("--report", "-r", help="Filename for report.") parser.add_argument("--hook", action="store_true", help="Install Git (Mercurial) hook.") + parser.add_argument( + "--async", action="store_true", + help="Enable async mode. Usefull for checking a lot of files. " + "Dont supported with pylint.") parser.add_argument( "--options", "-o", default=op.join(curdir, 'pylama.ini'), help="Select configuration file. By default is '/pylama.ini'") options = parser.parse_args() actions = dict((a.dest, a) for a in parser._actions) - # Setup LOGGER - LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) - if options.report: - LOGGER.removeHandler(STREAM) - LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) - # Read options from configuration file + from .inirama import Namespace + config = Namespace() config.default_section = 'main' - LOGGER.info('Try to read configuration from: ' + options.options) config.read(options.options) for k, v in config.default.items(): action = actions.get(k) if action: - LOGGER.info('Find option %s (%s)' % (k, v)) + LOGGER.info('Find option %s (%s)', k, v) name, value = action.dest, action.type(v)\ if callable(action.type) else v + if action.const: + value = bool(int(value)) setattr(options, name, value) + # Setup LOGGER + LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) + if options.report: + LOGGER.removeHandler(STREAM) + LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) + LOGGER.info('Try to read configuration from: ' + options.options) + # Install VSC hook if options.hook: from .hook import install_hook - return install_hook(options.path) + install_hook(options.path) + + else: - paths = [options.path] + paths = [options.path] - if op.isdir(options.path): - paths = [] - for root, _, files in walk(options.path): - paths += [op.join(root, f) for f in files if f.endswith('.py')] + if op.isdir(options.path): + paths = [] + for root, _, files in walk(options.path): + paths += [op.join(root, f) for f in files if f.endswith('.py')] - check_files( - paths, - rootpath=options.path, - skip=options.skip, - frmt=options.format, - ignore=options.ignore, - select=options.select, - linters=options.linters, - complexity=options.complexity, - config=config, - ) + check_files( + paths, + async=options.async, + rootpath=options.path, + skip=options.skip, + frmt=options.format, + ignore=options.ignore, + select=options.select, + linters=options.linters, + complexity=options.complexity, + config=config, + ) -def check_files(paths, rootpath=None, skip=None, frmt="pep8", +def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, select=None, ignore=None, linters=DEFAULT_LINTERS, complexity=DEFAULT_COMPLEXITY, config=None): + """ Check files. """ + from .tasks import async_check_files + rootpath = rootpath or getcwd() pattern = "%(rel)s:%(lnum)s:%(col)s: %(text)s" if frmt == 'pylint': @@ -195,49 +135,38 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", if key != 'main': params[op.abspath(key)] = prepare_params(section) - errors = [] - + work_paths = [] for path in paths: path = op.abspath(path) - if any(pattern.match(path) for pattern in skip): - LOGGER.info('Skip path: %s' % path) + if skip and any(pattern.match(path) for pattern in skip): + LOGGER.info('Skip path: %s', path) continue + work_paths.append(path) - LOGGER.info("Parse file: %s" % path) - errors = run(path, ignore=ignore, select=select, linters=linters, - complexity=complexity, config=params.get(path)) - for error in errors: - try: - error['rel'] = op.relpath( - error['filename'], op.dirname(rootpath)) - error['col'] = error.get('col', 1) - LOGGER.warning(pattern, error) - except KeyError: - continue - - sys.exit(int(bool(errors))) + errors = async_check_files( + work_paths, async=async, rootpath=rootpath, ignore=ignore, + select=select, linters=linters, complexity=complexity, params=params) + for error in errors: + LOGGER.warning(pattern, error) -MODERE = re.compile( - r'^\s*#\s+(?:pymode\:)?((?:lint[\w_]*=[^:\n\s]+:?)+)', re.I | re.M) + sys.exit(int(bool(errors))) -def parse_modeline(code): - seek = MODERE.search(code) - params = dict(lint=1) - if seek: - params = dict(v.split('=') for v in seek.group(1).split(':')) - params['lint'] = int(params.get('lint', 1)) - return params +def prepare_params(section): + """ Parse modeline params from configuration. + :return dict: Linter params. -def prepare_params(section): + """ params = dict(section) params['lint'] = int(params.get('lint', 1)) return params +def __parse_options(args=None): + pass + + if __name__ == '__main__': shell() - -# lint_ignore=R0914,C901,W0212 diff --git a/pylibs/pylama/pep257.py b/pylibs/pylama/pep257.py new file mode 100644 index 00000000..aab6a91f --- /dev/null +++ b/pylibs/pylama/pep257.py @@ -0,0 +1,676 @@ +#! /usr/bin/env python +"""Static analysis tool for checking docstring conventions and style. + +About +----- + +Currently implemented checks cover most of PEP257: +http://www.python.org/dev/peps/pep-0257/ + +After PEP257 is covered and tested, other checks might be added, +e.g. NumPy docstring conventions is the first candidate: +https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt + +The main repository of this program is located at: +http://github.com/GreenSteam/pep257 + +Creating own checks +------------------- + +In order to add your own check, create a function in "Checks functions" +section below. The function should take 3 parameters: + +docstring : str + Docstring to check, as it is in file (with quotes). +context : str + Docstring's context (e.g. function's source code). +is_script : bool + Whether the docstring is script with #! or not. + +Depending on 1st parameter name, the function will be called with +different type of docstring: + + * module_docstring + * function_docstring + * class_docstring + * method_docstring + * def_docstring (i.e. function-docstrings + method-docstrings) + * docstring (i.e. all above docstring types) + +E.g. the following function will be fed only class-docstrings: + + def your_check(class_docstring, context, is_script): + pass + +If for a certain function, class, etc. a docstring does not exist, +then `None` will be passed, which should be taken into account. + +To signify that a check passed successfully simply `return` from the +check function. If a check failed, return `True`. If a check failed +and you can provide the precise position where it failed, return a +tuple (start_position, end_position), where start and end positions +are integers specifying where in `context` the failure occured. + +Also, see examples in "Check functions" section. + +""" + +from curses.ascii import isascii +import inspect +from optparse import OptionParser +from os import walk +from os.path import abspath, basename, expanduser, isdir, isfile +from os.path import join as path_join +import re +import sys +import tokenize as tk + + +try: + from StringIO import StringIO +except ImportError: + # Python 3.0 and later + from io import StringIO + + +try: + all + any +except NameError: + # Python 2.4 and earlier + def all(iterable): + for element in iterable: + if not element: + return False + return True + + def any(iterable): + for element in iterable: + if element: + return True + return False + + +try: + next +except NameError: + # Python 2.5 and earlier + def next(obj): + return obj.next() + + +# +# Helper functions +# + +def cached(f): + """A decorator that caches function results. + + No cache expiration is currently done. + + """ + cache = {} + + def cached_func(*args, **kwargs): + key = (args, tuple(kwargs.items())) + if key in cache: + return cache[key] + else: + res = f(*args, **kwargs) + cache[key] = res + return res + return cached_func + + +def yield_list(f): + """Convert generator into list-returning function (decorator).""" + return lambda *arg, **kw: list(f(*arg, **kw)) + + +def remove_comments(s): + return re.sub('#[^\n]', '', s) + + +def abs_pos(marker, source): + """Return absolute position in source given (line, character) marker.""" + line, char = marker + lines = StringIO(source).readlines() + return len(''.join(lines[:line - 1])) + char + + +def rel_pos(abs_pos, source): + """Given absolute position, return relative (line, character) in source.""" + lines = StringIO(source).readlines() + nchars = len(source) + assert nchars >= abs_pos + while nchars > abs_pos: + assert nchars >= abs_pos + nchars -= len(lines[-1]) + lines.pop() + return len(lines) + 1, abs_pos - len(''.join(lines)) + + +# +# Parsing +# + + +def parse_module_docstring(source): + for kind, value, _, _, _ in tk.generate_tokens(StringIO(source).readline): + if kind in [tk.COMMENT, tk.NEWLINE, tk.NL]: + continue + elif kind == tk.STRING: # first STRING should be docstring + return value + else: + return None + + +def parse_docstring(source, what=''): + """Parse docstring given `def` or `class` source.""" + if what.startswith('module'): + return parse_module_docstring(source) + token_gen = tk.generate_tokens(StringIO(source).readline) + try: + kind = None + while kind != tk.INDENT: + kind, _, _, _, _ = next(token_gen) + kind, value, _, _, _ = next(token_gen) + if kind == tk.STRING: # STRING after INDENT is a docstring + return value + except StopIteration: + pass + + +@yield_list +def parse_top_level(source, keyword): + """Parse top-level functions or classes.""" + token_gen = tk.generate_tokens(StringIO(source).readline) + kind, value, char = None, None, None + while True: + start, end = None, None + while not (kind == tk.NAME and value == keyword and char == 0): + kind, value, (line, char), _, _ = next(token_gen) + start = line, char + while not (kind == tk.DEDENT and value == '' and char == 0): + kind, value, (line, char), _, _ = next(token_gen) + end = line, char + yield source[abs_pos(start, source): abs_pos(end, source)] + + +@cached +def parse_functions(source): + return parse_top_level(source, 'def') + + +@cached +def parse_classes(source): + return parse_top_level(source, 'class') + + +def skip_indented_block(token_gen): + kind, value, start, end, raw = next(token_gen) + while kind != tk.INDENT: + kind, value, start, end, raw = next(token_gen) + indent = 1 + for kind, value, start, end, raw in token_gen: + if kind == tk.INDENT: + indent += 1 + elif kind == tk.DEDENT: + indent -= 1 + if indent == 0: + return kind, value, start, end, raw + + +@cached +@yield_list +def parse_methods(source): + source = ''.join(parse_classes(source)) + token_gen = tk.generate_tokens(StringIO(source).readline) + kind, value, char = None, None, None + while True: + start, end = None, None + while not (kind == tk.NAME and value == 'def'): + kind, value, (line, char), _, _ = next(token_gen) + start = line, char + kind, value, (line, char), _, _ = skip_indented_block(token_gen) + end = line, char + yield source[abs_pos(start, source): abs_pos(end, source)] + + +def parse_contexts(source, kind): + if kind == 'module_docstring': + return [source] + if kind == 'function_docstring': + return parse_functions(source) + if kind == 'class_docstring': + return parse_classes(source) + if kind == 'method_docstring': + return parse_methods(source) + if kind == 'def_docstring': + return parse_functions(source) + parse_methods(source) + if kind == 'docstring': + return ([source] + parse_functions(source) + + parse_classes(source) + parse_methods(source)) + + +# +# Framework +# + + +class Error(object): + + """Error in docstring style. + + * Stores relevant data about the error, + * provides format for printing an error, + * provides __lt__ method to sort errors. + + """ + + # options that define how errors are printed + explain = False + range = False + quote = False + + def __init__(self, filename, source, docstring, context, + explanation, start=None, end=None): + self.filename = filename + self.source = source + self.docstring = docstring + self.context = context + self.explanation = explanation.strip() + + if start is None: + self.start = source.find(context) + context.find(docstring) + else: + self.start = source.find(context) + start + self.line, self.char = rel_pos(self.start, self.source) + + if end is None: + self.end = self.start + len(docstring) + else: + self.end = source.find(context) + end + self.end_line, self.end_char = rel_pos(self.end, self.source) + + def __str__(self): + s = self.filename + ':%d:%d' % (self.line, self.char) + if self.range: + s += '..%d:%d' % (self.end_line, self.end_char) + if self.explain: + s += ': ' + self.explanation + '\n' + else: + s += ': ' + self.explanation.split('\n')[0].strip() + if self.quote: + quote = self.source[self.start:self.end].strip() + s += '\n> ' + '\n> '.join(quote.split('\n')) + '\n' + return s + + def __lt__(self, other): + return (self.filename, self.start) < (other.filename, other.start) + + +@yield_list +def find_checks(keyword): + for function in globals().values(): + if inspect.isfunction(function): + args = inspect.getargspec(function)[0] + if args and args[0] == keyword: + yield function + + +@yield_list +def check_source(source, filename): + keywords = ['module_docstring', 'function_docstring', + 'class_docstring', 'method_docstring', + 'def_docstring', 'docstring'] # TODO? 'nested_docstring'] + is_script = source.startswith('#!') or \ + basename(filename).startswith('test_') + for keyword in keywords: + for check in find_checks(keyword): + for context in parse_contexts(source, keyword): + docstring = parse_docstring(context, keyword) + result = check(docstring, context, is_script) + if result: + positions = [] if result is True else result + yield Error(filename, source, docstring, context, + check.__doc__, *positions) + + +def find_input_files(filenames): + """ Return a list of input files. + + `filenames` is a list of filenames, which may be either files + or directories. Files within subdirectories are added + recursively. + + """ + input_files = [] + + filenames = [abspath(expanduser(f)) for f in filenames] + for filename in filenames: + if isdir(filename): + for root, _dirs, files in walk(filename): + input_files += [path_join(root, f) for f in sorted(files) + if f.endswith(".py")] + elif isfile(filename): + input_files += [filename] + else: + print_error("%s is not a file or directory" % filename) + + return input_files + + +def check_files(filenames): + r"""Return list of docstring style errors found in files. + + Example + ------- + >>> import pep257 + >>> pep257.check_files(['one.py', 'two.py']) + ['one.py:23:1 PEP257 Use u\"\"\" for Unicode docstrings.'] + + """ + errors = [] + for filename in find_input_files(filenames): + errors.extend(check_source(open(filename).read(), filename)) + return [str(e) for e in errors] + + +def parse_options(): + parser = OptionParser() + parser.add_option('-e', '--explain', action='store_true', + help='show explanation of each error') + parser.add_option('-r', '--range', action='store_true', + help='show error start..end positions') + parser.add_option('-q', '--quote', action='store_true', + help='quote erroneous lines') + return parser.parse_args() + + +def print_error(message): + sys.stderr.write(message) + sys.stderr.write('\n') + sys.stderr.flush() + + +def main(options, arguments): + print('=' * 80) + print('Note: checks are relaxed for scripts (with #!) compared to modules') + Error.explain = options.explain + Error.range = options.range + Error.quote = options.quote + errors = [] + + for filename in find_input_files(arguments): + try: + f = open(filename) + except IOError: + print_error("Error opening file %s" % filename) + else: + try: + errors.extend(check_source(f.read(), filename)) + except IOError: + print_error("Error reading file %s" % filename) + except tk.TokenError: + print_error("Error parsing file %s" % filename) + finally: + f.close() + for error in sorted(errors): + print_error(str(error)) + + +# +# Check functions +# + + +def check_modules_have_docstrings(module_docstring, context, is_script): + """All modules should have docstrings. + + All modules should normally have docstrings. + + """ + if not module_docstring: # or not eval(module_docstring).strip(): + return 0, min(79, len(context)) + if not eval(module_docstring).strip(): + return True + + +def check_def_has_docstring(def_docstring, context, is_script): + """Exported definitions should have docstrings. + + ...all functions and classes exported by a module should also have + docstrings. Public methods (including the __init__ constructor) + should also have docstrings. + + """ + if is_script: + return # assume nothing is exported + def_name = context.split()[1] + if def_name.startswith('_') and not def_name.endswith('__'): + return # private, not exported + if not def_docstring: + return 0, len(context.split('\n')[0]) + if not eval(def_docstring).strip(): + return True + + +def check_class_has_docstring(class_docstring, context, is_script): + """Exported classes should have docstrings. + + ...all functions and classes exported by a module should also have + docstrings. + + """ + if is_script: + return # assume nothing is exported + class_name = context.split()[1] + if class_name.startswith('_'): + return # not exported + if not class_docstring: + return 0, len(context.split('\n')[0]) + if not eval(class_docstring).strip(): + return True + + +def check_triple_double_quotes(docstring, context, is_script): + r"""Use \"\"\"triple double quotes\"\"\". + + For consistency, always use \"\"\"triple double quotes\"\"\" around + docstrings. Use r\"\"\"raw triple double quotes\"\"\" if you use any + backslashes in your docstrings. For Unicode docstrings, use + u\"\"\"Unicode triple-quoted strings\"\"\". + + """ + if docstring and not (docstring.startswith('"""') or + docstring.startswith('r"""') or + docstring.startswith('u"""')): + return True + + +def check_backslashes(docstring, context, is_script): + r"""Use r\"\"\" if any backslashes in your docstrings. + + Use r\"\"\"raw triple double quotes\"\"\" if you use any backslashes + (\\) in your docstrings. + + """ + if docstring and "\\" in docstring and not docstring.startswith('r"""'): + return True + + +def check_unicode_docstring(docstring, context, is_script): + r"""Use u\"\"\" for Unicode docstrings. + + For Unicode docstrings, use u\"\"\"Unicode triple-quoted stringsr\"\"\". + + """ + if (docstring and not all(isascii(char) for char in docstring) and + not docstring.startswith('u"""')): + return True + + +def check_one_liners(docstring, context, is_script): + """One-liner docstrings should fit on one line with quotes. + + The closing quotes are on the same line as the opening quotes. + This looks better for one-liners. + + """ + if not docstring: + return + lines = docstring.split('\n') + if len(lines) > 1: + non_empty = [l for l in lines if any([c.isalpha() for c in l])] + if len(non_empty) == 1: + return True + + +def check_no_blank_before(def_docstring, context, is_script): + """No blank line before docstring in definitions. + + There's no blank line either before or after the docstring. + + """ + if not def_docstring: + return + before = remove_comments(context.split(def_docstring)[0]) + if before.split(':')[-1].count('\n') > 1: + return True + + +def check_ends_with_period(docstring, context, is_script): + """First line should end with a period. + + The [first line of a] docstring is a phrase ending in a period. + + """ + if docstring and not eval(docstring).split('\n')[0].strip().endswith('.'): + return True + + +def check_imperative_mood(def_docstring, context, is_script): + """First line should be in imperative mood ('Do', not 'Does'). + + [Docstring] prescribes the function or method's effect as a command: + ("Do this", "Return that"), not as a description; e.g. don't write + "Returns the pathname ...". + + """ + if def_docstring and eval(def_docstring).strip(): + first_word = eval(def_docstring).strip().split()[0] + if first_word.endswith('s') and not first_word.endswith('ss'): + return True + + +def check_no_signature(def_docstring, context, is_script): + """First line should not be function's or method's "signature". + + The one-line docstring should NOT be a "signature" reiterating + the function/method parameters (which can be obtained by introspection). + + """ + if not def_docstring: + return + def_name = context.split(def_docstring)[0].split()[1].split('(')[0] + first_line = eval(def_docstring).split('\n')[0] + if def_name + '(' in first_line.replace(' ', ''): + return True + + +def check_return_type(def_docstring, context, is_script): + """Return value type should be mentioned. + + However, the nature of the return value cannot be determined by + introspection, so it should be mentioned. + + """ + if (not def_docstring) or is_script: + return + if 'return' not in def_docstring.lower(): + tokens = list(tk.generate_tokens(StringIO(context).readline)) + after_return = [tokens[i + 1][0] for i, token in enumerate(tokens) + if token[1] == 'return'] + # not very precise (tk.OP ';' is not taken into account) + if set(after_return) - set([tk.COMMENT, tk.NL, tk.NEWLINE]) != set([]): + return True + + +def check_blank_after_summary(docstring, context, is_script): + """Blank line missing after one-line summary. + + Multi-line docstrings consist of a summary line just like a one-line + docstring, followed by a blank line, followed by a more elaborate + description. The summary line may be used by automatic indexing tools; + it is important that it fits on one line and is separated from the + rest of the docstring by a blank line. + + """ + if not docstring: + return + lines = eval(docstring).split('\n') + if len(lines) > 1 and lines[1].strip() != '': + return True + + +def check_indent(docstring, context, is_script): + """The entire docstring should be indented same as code. + + The entire docstring is indented the same as the quotes at its + first line. + + """ + if (not docstring) or len(eval(docstring).split('\n')) == 1: + return + non_empty_lines = [line for line in eval(docstring).split('\n')[1:] + if line.strip()] + if not non_empty_lines: + return + indent = min([len(l) - len(l.lstrip()) for l in non_empty_lines]) + if indent != len(context.split(docstring)[0].split('\n')[-1]): + return True + + +def check_blank_before_after_class(class_docstring, context, is_script): + """Class docstring should have 1 blank line around them. + + Insert a blank line before and after all docstrings (one-line or + multi-line) that document a class -- generally speaking, the class's + methods are separated from each other by a single blank line, and the + docstring needs to be offset from the first method by a blank line; + for symmetry, put a blank line between the class header and the + docstring. + + """ + if not class_docstring: + return + before, after = context.split(class_docstring) + before_blanks = [not line.strip() for line in before.split('\n')] + after_blanks = [not line.strip() for line in after.split('\n')] + if before_blanks[-3:] != [False, True, True]: + return True + if not all(after_blanks) and after_blanks[:3] != [True, True, False]: + return True + + +def check_blank_after_last_paragraph(docstring, context, is_script): + """Multiline docstring should end with 1 blank line. + + The BDFL recommends inserting a blank line between the last + paragraph in a multi-line docstring and its closing quotes, + placing the closing quotes on a line by themselves. + + """ + if (not docstring) or len(eval(docstring).split('\n')) == 1: + return + blanks = [not line.strip() for line in eval(docstring).split('\n')] + if blanks[-3:] != [False, True, True]: + return True + + +if __name__ == '__main__': + try: + main(*parse_options()) + except KeyboardInterrupt: + pass diff --git a/pylibs/pylama/tasks.py b/pylibs/pylama/tasks.py new file mode 100644 index 00000000..3d301c04 --- /dev/null +++ b/pylibs/pylama/tasks.py @@ -0,0 +1,105 @@ +""" Async code checking. +""" +import logging +import threading +from os import path as op +try: + import Queue +except ImportError: + import queue as Queue + +from .core import run + + +try: + import multiprocessing + + CPU_COUNT = multiprocessing.cpu_count() + +except (ImportError, NotImplementedError): + CPU_COUNT = 1 + +LOGGER = logging.getLogger('pylama') + + +class Worker(threading.Thread): + + """ Get tasks from queue and run. """ + + def __init__(self, path_queue, result_queue): + threading.Thread.__init__(self) + self.path_queue = path_queue + self.result_queue = result_queue + + def run(self): + """ Run tasks from queue. """ + while True: + path, params = self.path_queue.get() + errors = check_path(path, **params) + self.result_queue.put(errors) + self.path_queue.task_done() + + +def async_check_files(paths, async=False, linters=None, **params): + """ Check paths. + + :return list: list of errors + + """ + + errors = [] + + # Disable async if pylint enabled + async = async and not 'pylint' in linters + params['linters'] = linters + + if not async: + for path in paths: + errors += check_path(path, **params) + return errors + + LOGGER.info('Async code checking is enabled.') + path_queue = Queue.Queue() + result_queue = Queue.Queue() + + for _ in range(CPU_COUNT): + worker = Worker(path_queue, result_queue) + worker.setDaemon(True) + worker.start() + + for path in paths: + path_queue.put((path, params)) + + path_queue.join() + + while True: + try: + errors += result_queue.get(False) + except Queue.Empty: + break + + return errors + + +def check_path(path, rootpath='.', ignore=None, select=None, linters=None, + complexity=None, params=None): + """ Check path. + + :return list: list of errors + + """ + + LOGGER.info("Parse file: %s", path) + params = params or dict() + + errors = [] + for error in run(path, ignore=ignore, select=select, linters=linters, + complexity=complexity, config=params.get(path)): + try: + error['rel'] = op.relpath( + error['filename'], op.dirname(rootpath)) + error['col'] = error.get('col', 1) + errors.append(error) + except KeyError: + continue + return errors diff --git a/pylibs/pylama/utils.py b/pylibs/pylama/utils.py index 78460ce4..2a135cf8 100644 --- a/pylibs/pylama/utils.py +++ b/pylibs/pylama/utils.py @@ -1,31 +1,33 @@ +""" Interfaces for code checking. +""" from __future__ import absolute_import, with_statement import _ast from os import path as op, environ -from .mccabe import get_code_complexity from .pep8 import BaseReport, StyleGuide -from .pyflakes import checker -__all__ = 'pep8', 'mccabe', 'pyflakes', 'pylint' +__all__ = 'pep8', 'pep257', 'mccabe', 'pyflakes', 'pylint' PYLINT_RC = op.abspath(op.join(op.dirname(__file__), 'pylint.rc')) -class PEP8Report(BaseReport): +class _PEP8Report(BaseReport): def __init__(self, *args, **kwargs): - super(PEP8Report, self).__init__(*args, **kwargs) + super(_PEP8Report, self).__init__(*args, **kwargs) self.errors = [] def init_file(self, filename, lines, expected, line_offset): - super(PEP8Report, self).init_file( + """ Prepare storage for errors. """ + super(_PEP8Report, self).init_file( filename, lines, expected, line_offset) self.errors = [] def error(self, line_number, offset, text, check): - code = super(PEP8Report, self).error( + """ Save errors. """ + code = super(_PEP8Report, self).error( line_number, offset, text, check) self.errors.append(dict( @@ -36,25 +38,42 @@ def error(self, line_number, offset, text, check): )) def get_file_results(self): - return self.errors + """ Get errors. + + :return list: List of errors. -P8Style = StyleGuide(reporter=PEP8Report) + """ + return self.errors def pep8(path, **meta): - " PEP8 code checking. " + """ PEP8 code checking. + :return list: List of errors. + + """ + P8Style = StyleGuide(reporter=_PEP8Report) return P8Style.input_file(path) def mccabe(path, code=None, complexity=8, **meta): - " MCCabe code checking. " + """ MCCabe code checking. + + :return list: List of errors. + + """ + from .mccabe import get_code_complexity return get_code_complexity(code, complexity, filename=path) or [] def pyflakes(path, code=None, **meta): - " PyFlakes code checking. " + """ Pyflake code checking. + + :return list: List of errors. + + """ + from .pyflakes import checker errors = [] tree = compile(code, path, "exec", _ast.PyCF_ONLY_AST) @@ -69,6 +88,11 @@ def pyflakes(path, code=None, **meta): def pylint(path, **meta): + """ Pylint code checking. + + :return list: List of errors. + + """ from sys import version_info if version_info > (3, 0): import logging @@ -77,8 +101,8 @@ def pylint(path, **meta): from .pylint.lint import Run from .pylint.reporters import BaseReporter + from .pylint.logilab.astng import MANAGER - from .pylint.logilab.astng.builder import MANAGER MANAGER.astng_cache.clear() class Reporter(BaseReporter): @@ -109,4 +133,25 @@ def add_message(self, msg_id, location, msg): [path] + attrs, reporter=Reporter(), exit=False) return runner.linter.reporter.errors + +def pep257(path, **meta): + """ PEP257 code checking. + + :return list: List of errors. + + """ + f = open(path) + from .pep257 import check_source + + errors = [] + for er in check_source(f.read(), path): + errors.append(dict( + lnum=er.line, + col=er.char, + text='C0110 %s' % er.explanation.split('\n')[0].strip(), + type='W', + )) + return errors + + # pymode:lint_ignore=W0231 diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index 51930bce..0f315c8a 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -3,7 +3,8 @@ import locale import json -from pylama.main import run, prepare_params +from pylama.core import run +from pylama.main import prepare_params from pylama.inirama import Namespace from os import path as op From d17f039cbfdd231979c37cfb9f1b407cfaf7dec9 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Fri, 7 Jun 2013 11:01:58 +0800 Subject: [PATCH 030/582] Update docs --- Changelog.rst | 1 + README.rst | 1 + doc/pymode.txt | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.rst b/Changelog.rst index f6fdb811..a212925e 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -3,6 +3,7 @@ Changelog * Added `g:pymode_rope_autocomplete_map` option; * Removed `g:pymode_rope_map_space` option; +* Added PEP257 checker ## 2013-05-15 0.6.18 -------------------- diff --git a/README.rst b/README.rst index deb602c5..a8f67c24 100644 --- a/README.rst +++ b/README.rst @@ -144,6 +144,7 @@ Default values: :: " Switch pylint, pyflakes, pep8, mccabe code-checkers " Can have multiply values "pep8,pyflakes,mcccabe" + " Choices are pyflakes, pep8, mccabe, pylint, pep257 let g:pymode_lint_checker = "pyflakes,pep8,mccabe" " Skip errors and warnings diff --git a/doc/pymode.txt b/doc/pymode.txt index af8917b8..01c150f6 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -223,7 +223,7 @@ If this option is set to 0 then the pylint script is disabled. ------------------------------------------------------------------------------ *'pymode_lint_checker'* -Values: "pylint", "pyflakes", "pep8", "mccabe" +Values: "pylint", "pyflakes", "pep8", "mccabe", "pep257" You can set many checkers. E.g. "pyflakes,pep8,mccabe" ~ Default: "pyflakes,pep8,mccabe". From 70d8eab3bee42fbde292b4fa2813e69d9a5e45fe Mon Sep 17 00:00:00 2001 From: Jan Gosmann Date: Mon, 10 Jun 2013 13:54:52 +0200 Subject: [PATCH 031/582] Fix slow saving by not clearing cache every time. --- pylibs/rope/base/pycore.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pylibs/rope/base/pycore.py b/pylibs/rope/base/pycore.py index 32056a0f..164f669c 100644 --- a/pylibs/rope/base/pycore.py +++ b/pylibs/rope/base/pycore.py @@ -258,7 +258,6 @@ def analyze_module(self, resource, should_analyze=lambda py: True, if followed_calls is None: followed_calls = self.project.prefs.get('soa_followed_calls', 0) pymodule = self.resource_to_pyobject(resource) - self.module_cache.forget_all_data() rope.base.oi.soa.analyze_module( self, pymodule, should_analyze, search_subscopes, followed_calls) @@ -306,7 +305,7 @@ def __init__(self, pycore): def _invalidate_resource(self, resource): if resource in self.module_map: - self.forget_all_data() + self.module_map[resource]._forget_concluded_data() self.observer.remove_resource(resource) del self.module_map[resource] From 328111ebfba91826ca21fb78ccff3523dfd37853 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Sat, 15 Jun 2013 16:39:43 +0100 Subject: [PATCH 032/582] Remove leftovers of auto session support. Should have been removed on a previous commit. --- autoload/pymode/troubleshooting.vim | 1 - ftplugin/python/init-pymode.vim | 8 -------- 2 files changed, 9 deletions(-) diff --git a/autoload/pymode/troubleshooting.vim b/autoload/pymode/troubleshooting.vim index dca1ea7d..1c883a6e 100644 --- a/autoload/pymode/troubleshooting.vim +++ b/autoload/pymode/troubleshooting.vim @@ -71,7 +71,6 @@ fun! pymode#troubleshooting#Test() "{{{ call append('$', 'let pymode_rope_autocomplete_map = ' . string(g:pymode_rope_autocomplete_map)) call append('$', 'let pymode_rope_auto_project = ' . string(g:pymode_rope_auto_project)) call append('$', 'let pymode_rope_auto_project_open = ' . string(g:pymode_rope_auto_project_open)) - call append('$', 'let pymode_rope_auto_session_manage = ' . string(g:pymode_rope_auto_session_manage)) end call append('$', 'let pymode_folding = ' . string(g:pymode_folding)) call append('$', 'let pymode_breakpoint = ' . string(g:pymode_breakpoint)) diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 59e287ea..2998e14e 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -201,9 +201,6 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope " `.ropeproject` subdirectory. call pymode#Default("g:pymode_rope_auto_project_open", 1) - " OPTION: g:pymode_rope_auto_session_manage -- bool - call pymode#Default("g:pymode_rope_auto_session_manage", 0) - " OPTION: g:pymode_rope_enable_autoimport -- bool. Enable autoimport call pymode#Default("g:pymode_rope_enable_autoimport", 1) @@ -313,11 +310,6 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope call RopeOpenExistingProject() endif - if !pymode#Default("g:pymode_rope_auto_session_manage", 0) || g:pymode_rope_auto_session_manage - autocmd VimLeave * call RopeSaveSession() - autocmd VimEnter * call RopeRestoreSession() - endif - endif " }}} From 740bd743ec96cd3d7866effddbe8eb61a28df12c Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 20 Jun 2013 23:09:35 +0800 Subject: [PATCH 033/582] Update pylama --- pylibs/pylama/__init__.py | 2 +- .../{pylint/logilab => checkers}/__init__.py | 0 pylibs/pylama/{ => checkers}/mccabe.py | 0 pylibs/pylama/{ => checkers}/pep257.py | 0 pylibs/pylama/{ => checkers}/pep8.py | 0 .../{ => checkers}/pyflakes/__init__.py | 0 .../pylama/{ => checkers}/pyflakes/checker.py | 0 .../{ => checkers}/pyflakes/messages.py | 0 .../pylama/{ => checkers}/pylint/__init__.py | 0 .../{ => checkers}/pylint/__pkginfo__.py | 0 .../pylint/checkers/__init__.py | 0 .../{ => checkers}/pylint/checkers/base.py | 0 .../{ => checkers}/pylint/checkers/classes.py | 0 .../pylint/checkers/design_analysis.py | 0 .../pylint/checkers/exceptions.py | 0 .../{ => checkers}/pylint/checkers/format.py | 0 .../{ => checkers}/pylint/checkers/imports.py | 0 .../{ => checkers}/pylint/checkers/logging.py | 0 .../{ => checkers}/pylint/checkers/misc.py | 0 .../pylint/checkers/newstyle.py | 0 .../pylint/checkers/raw_metrics.py | 0 .../{ => checkers}/pylint/checkers/similar.py | 0 .../{ => checkers}/pylint/checkers/strings.py | 0 .../pylint/checkers/typecheck.py | 0 .../{ => checkers}/pylint/checkers/utils.py | 0 .../pylint/checkers/variables.py | 0 pylibs/pylama/{ => checkers}/pylint/config.py | 0 .../{ => checkers}/pylint/interfaces.py | 0 pylibs/pylama/{ => checkers}/pylint/lint.py | 0 .../pylint/logilab}/__init__.py | 0 .../pylint/logilab/astng/__init__.py | 0 .../pylint/logilab/astng/__pkginfo__.py | 0 .../pylint/logilab/astng/as_string.py | 0 .../pylint/logilab/astng/bases.py | 0 .../pylint/logilab/astng/brain/__init__.py | 0 .../logilab/astng/brain/py2mechanize.py | 0 .../pylint/logilab/astng/brain/py2qt4.py | 0 .../pylint/logilab/astng/brain/py2stdlib.py | 0 .../pylint/logilab/astng/builder.py | 0 .../pylint/logilab/astng/exceptions.py | 0 .../pylint/logilab/astng/inference.py | 0 .../pylint/logilab/astng/manager.py | 0 .../pylint/logilab/astng/mixins.py | 0 .../pylint/logilab/astng/node_classes.py | 0 .../pylint/logilab/astng/nodes.py | 0 .../pylint/logilab/astng/protocols.py | 0 .../pylint/logilab/astng/raw_building.py | 0 .../pylint/logilab/astng/rebuilder.py | 0 .../pylint/logilab/astng/scoped_nodes.py | 0 .../pylint/logilab/astng/utils.py | 0 .../pylint/logilab/common/__init__.py | 0 .../pylint/logilab/common/__pkginfo__.py | 0 .../pylint/logilab/common/changelog.py | 0 .../pylint/logilab/common/compat.py | 0 .../pylint/logilab/common/configuration.py | 0 .../pylint/logilab/common/decorators.py | 0 .../pylint/logilab/common/deprecation.py | 0 .../pylint/logilab/common/graph.py | 0 .../pylint/logilab/common/interface.py | 0 .../pylint/logilab/common/modutils.py | 0 .../pylint/logilab/common/optik_ext.py | 0 .../pylint/logilab/common/textutils.py | 0 .../pylint/logilab/common/tree.py | 0 .../logilab/common/ureports/__init__.py | 0 .../logilab/common/ureports/docbook_writer.py | 0 .../logilab/common/ureports/html_writer.py | 0 .../pylint/logilab/common/ureports/nodes.py | 0 .../logilab/common/ureports/text_writer.py | 0 .../pylint/logilab/common/visitor.py | 0 .../pylint/reporters/__init__.py | 0 .../pylint/reporters/guireporter.py | 0 .../{ => checkers}/pylint/reporters/html.py | 0 .../{ => checkers}/pylint/reporters/text.py | 0 pylibs/pylama/{ => checkers}/pylint/utils.py | 0 pylibs/pylama/core.py | 7 +- pylibs/pylama/hook.py | 12 ++- pylibs/pylama/inirama.py | 100 ++++++++++++------ pylibs/pylama/main.py | 16 ++- pylibs/pylama/tasks.py | 10 +- pylibs/pylama/utils.py | 14 +-- pylibs/pymode/lint.py | 22 +++- 81 files changed, 122 insertions(+), 61 deletions(-) rename pylibs/pylama/{pylint/logilab => checkers}/__init__.py (100%) rename pylibs/pylama/{ => checkers}/mccabe.py (100%) rename pylibs/pylama/{ => checkers}/pep257.py (100%) rename pylibs/pylama/{ => checkers}/pep8.py (100%) rename pylibs/pylama/{ => checkers}/pyflakes/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pyflakes/checker.py (100%) rename pylibs/pylama/{ => checkers}/pyflakes/messages.py (100%) rename pylibs/pylama/{ => checkers}/pylint/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/__pkginfo__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/base.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/classes.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/design_analysis.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/exceptions.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/format.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/imports.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/logging.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/misc.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/newstyle.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/raw_metrics.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/similar.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/strings.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/typecheck.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/utils.py (100%) rename pylibs/pylama/{ => checkers}/pylint/checkers/variables.py (100%) rename pylibs/pylama/{ => checkers}/pylint/config.py (100%) rename pylibs/pylama/{ => checkers}/pylint/interfaces.py (100%) rename pylibs/pylama/{ => checkers}/pylint/lint.py (100%) rename pylibs/pylama/{pylint/logilab/astng/brain => checkers/pylint/logilab}/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/__pkginfo__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/as_string.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/bases.py (100%) create mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/__init__.py rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/brain/py2mechanize.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/brain/py2qt4.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/brain/py2stdlib.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/builder.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/exceptions.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/inference.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/manager.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/mixins.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/node_classes.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/nodes.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/protocols.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/raw_building.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/rebuilder.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/scoped_nodes.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/astng/utils.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/__pkginfo__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/changelog.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/compat.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/configuration.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/decorators.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/deprecation.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/graph.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/interface.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/modutils.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/optik_ext.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/textutils.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/tree.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/ureports/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/ureports/docbook_writer.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/ureports/html_writer.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/ureports/nodes.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/ureports/text_writer.py (100%) rename pylibs/pylama/{ => checkers}/pylint/logilab/common/visitor.py (100%) rename pylibs/pylama/{ => checkers}/pylint/reporters/__init__.py (100%) rename pylibs/pylama/{ => checkers}/pylint/reporters/guireporter.py (100%) rename pylibs/pylama/{ => checkers}/pylint/reporters/html.py (100%) rename pylibs/pylama/{ => checkers}/pylint/reporters/text.py (100%) rename pylibs/pylama/{ => checkers}/pylint/utils.py (100%) diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 6d528b30..4cc579fc 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -5,7 +5,7 @@ :license: BSD, see LICENSE for more details. """ -version_info = 1, 0, 4 +version_info = 1, 1, 0 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/pylint/logilab/__init__.py b/pylibs/pylama/checkers/__init__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/__init__.py rename to pylibs/pylama/checkers/__init__.py diff --git a/pylibs/pylama/mccabe.py b/pylibs/pylama/checkers/mccabe.py similarity index 100% rename from pylibs/pylama/mccabe.py rename to pylibs/pylama/checkers/mccabe.py diff --git a/pylibs/pylama/pep257.py b/pylibs/pylama/checkers/pep257.py similarity index 100% rename from pylibs/pylama/pep257.py rename to pylibs/pylama/checkers/pep257.py diff --git a/pylibs/pylama/pep8.py b/pylibs/pylama/checkers/pep8.py similarity index 100% rename from pylibs/pylama/pep8.py rename to pylibs/pylama/checkers/pep8.py diff --git a/pylibs/pylama/pyflakes/__init__.py b/pylibs/pylama/checkers/pyflakes/__init__.py similarity index 100% rename from pylibs/pylama/pyflakes/__init__.py rename to pylibs/pylama/checkers/pyflakes/__init__.py diff --git a/pylibs/pylama/pyflakes/checker.py b/pylibs/pylama/checkers/pyflakes/checker.py similarity index 100% rename from pylibs/pylama/pyflakes/checker.py rename to pylibs/pylama/checkers/pyflakes/checker.py diff --git a/pylibs/pylama/pyflakes/messages.py b/pylibs/pylama/checkers/pyflakes/messages.py similarity index 100% rename from pylibs/pylama/pyflakes/messages.py rename to pylibs/pylama/checkers/pyflakes/messages.py diff --git a/pylibs/pylama/pylint/__init__.py b/pylibs/pylama/checkers/pylint/__init__.py similarity index 100% rename from pylibs/pylama/pylint/__init__.py rename to pylibs/pylama/checkers/pylint/__init__.py diff --git a/pylibs/pylama/pylint/__pkginfo__.py b/pylibs/pylama/checkers/pylint/__pkginfo__.py similarity index 100% rename from pylibs/pylama/pylint/__pkginfo__.py rename to pylibs/pylama/checkers/pylint/__pkginfo__.py diff --git a/pylibs/pylama/pylint/checkers/__init__.py b/pylibs/pylama/checkers/pylint/checkers/__init__.py similarity index 100% rename from pylibs/pylama/pylint/checkers/__init__.py rename to pylibs/pylama/checkers/pylint/checkers/__init__.py diff --git a/pylibs/pylama/pylint/checkers/base.py b/pylibs/pylama/checkers/pylint/checkers/base.py similarity index 100% rename from pylibs/pylama/pylint/checkers/base.py rename to pylibs/pylama/checkers/pylint/checkers/base.py diff --git a/pylibs/pylama/pylint/checkers/classes.py b/pylibs/pylama/checkers/pylint/checkers/classes.py similarity index 100% rename from pylibs/pylama/pylint/checkers/classes.py rename to pylibs/pylama/checkers/pylint/checkers/classes.py diff --git a/pylibs/pylama/pylint/checkers/design_analysis.py b/pylibs/pylama/checkers/pylint/checkers/design_analysis.py similarity index 100% rename from pylibs/pylama/pylint/checkers/design_analysis.py rename to pylibs/pylama/checkers/pylint/checkers/design_analysis.py diff --git a/pylibs/pylama/pylint/checkers/exceptions.py b/pylibs/pylama/checkers/pylint/checkers/exceptions.py similarity index 100% rename from pylibs/pylama/pylint/checkers/exceptions.py rename to pylibs/pylama/checkers/pylint/checkers/exceptions.py diff --git a/pylibs/pylama/pylint/checkers/format.py b/pylibs/pylama/checkers/pylint/checkers/format.py similarity index 100% rename from pylibs/pylama/pylint/checkers/format.py rename to pylibs/pylama/checkers/pylint/checkers/format.py diff --git a/pylibs/pylama/pylint/checkers/imports.py b/pylibs/pylama/checkers/pylint/checkers/imports.py similarity index 100% rename from pylibs/pylama/pylint/checkers/imports.py rename to pylibs/pylama/checkers/pylint/checkers/imports.py diff --git a/pylibs/pylama/pylint/checkers/logging.py b/pylibs/pylama/checkers/pylint/checkers/logging.py similarity index 100% rename from pylibs/pylama/pylint/checkers/logging.py rename to pylibs/pylama/checkers/pylint/checkers/logging.py diff --git a/pylibs/pylama/pylint/checkers/misc.py b/pylibs/pylama/checkers/pylint/checkers/misc.py similarity index 100% rename from pylibs/pylama/pylint/checkers/misc.py rename to pylibs/pylama/checkers/pylint/checkers/misc.py diff --git a/pylibs/pylama/pylint/checkers/newstyle.py b/pylibs/pylama/checkers/pylint/checkers/newstyle.py similarity index 100% rename from pylibs/pylama/pylint/checkers/newstyle.py rename to pylibs/pylama/checkers/pylint/checkers/newstyle.py diff --git a/pylibs/pylama/pylint/checkers/raw_metrics.py b/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py similarity index 100% rename from pylibs/pylama/pylint/checkers/raw_metrics.py rename to pylibs/pylama/checkers/pylint/checkers/raw_metrics.py diff --git a/pylibs/pylama/pylint/checkers/similar.py b/pylibs/pylama/checkers/pylint/checkers/similar.py similarity index 100% rename from pylibs/pylama/pylint/checkers/similar.py rename to pylibs/pylama/checkers/pylint/checkers/similar.py diff --git a/pylibs/pylama/pylint/checkers/strings.py b/pylibs/pylama/checkers/pylint/checkers/strings.py similarity index 100% rename from pylibs/pylama/pylint/checkers/strings.py rename to pylibs/pylama/checkers/pylint/checkers/strings.py diff --git a/pylibs/pylama/pylint/checkers/typecheck.py b/pylibs/pylama/checkers/pylint/checkers/typecheck.py similarity index 100% rename from pylibs/pylama/pylint/checkers/typecheck.py rename to pylibs/pylama/checkers/pylint/checkers/typecheck.py diff --git a/pylibs/pylama/pylint/checkers/utils.py b/pylibs/pylama/checkers/pylint/checkers/utils.py similarity index 100% rename from pylibs/pylama/pylint/checkers/utils.py rename to pylibs/pylama/checkers/pylint/checkers/utils.py diff --git a/pylibs/pylama/pylint/checkers/variables.py b/pylibs/pylama/checkers/pylint/checkers/variables.py similarity index 100% rename from pylibs/pylama/pylint/checkers/variables.py rename to pylibs/pylama/checkers/pylint/checkers/variables.py diff --git a/pylibs/pylama/pylint/config.py b/pylibs/pylama/checkers/pylint/config.py similarity index 100% rename from pylibs/pylama/pylint/config.py rename to pylibs/pylama/checkers/pylint/config.py diff --git a/pylibs/pylama/pylint/interfaces.py b/pylibs/pylama/checkers/pylint/interfaces.py similarity index 100% rename from pylibs/pylama/pylint/interfaces.py rename to pylibs/pylama/checkers/pylint/interfaces.py diff --git a/pylibs/pylama/pylint/lint.py b/pylibs/pylama/checkers/pylint/lint.py similarity index 100% rename from pylibs/pylama/pylint/lint.py rename to pylibs/pylama/checkers/pylint/lint.py diff --git a/pylibs/pylama/pylint/logilab/astng/brain/__init__.py b/pylibs/pylama/checkers/pylint/logilab/__init__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/brain/__init__.py rename to pylibs/pylama/checkers/pylint/logilab/__init__.py diff --git a/pylibs/pylama/pylint/logilab/astng/__init__.py b/pylibs/pylama/checkers/pylint/logilab/astng/__init__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/__init__.py rename to pylibs/pylama/checkers/pylint/logilab/astng/__init__.py diff --git a/pylibs/pylama/pylint/logilab/astng/__pkginfo__.py b/pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/__pkginfo__.py rename to pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py diff --git a/pylibs/pylama/pylint/logilab/astng/as_string.py b/pylibs/pylama/checkers/pylint/logilab/astng/as_string.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/as_string.py rename to pylibs/pylama/checkers/pylint/logilab/astng/as_string.py diff --git a/pylibs/pylama/pylint/logilab/astng/bases.py b/pylibs/pylama/checkers/pylint/logilab/astng/bases.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/bases.py rename to pylibs/pylama/checkers/pylint/logilab/astng/bases.py diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/brain/__init__.py b/pylibs/pylama/checkers/pylint/logilab/astng/brain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pylibs/pylama/pylint/logilab/astng/brain/py2mechanize.py b/pylibs/pylama/checkers/pylint/logilab/astng/brain/py2mechanize.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/brain/py2mechanize.py rename to pylibs/pylama/checkers/pylint/logilab/astng/brain/py2mechanize.py diff --git a/pylibs/pylama/pylint/logilab/astng/brain/py2qt4.py b/pylibs/pylama/checkers/pylint/logilab/astng/brain/py2qt4.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/brain/py2qt4.py rename to pylibs/pylama/checkers/pylint/logilab/astng/brain/py2qt4.py diff --git a/pylibs/pylama/pylint/logilab/astng/brain/py2stdlib.py b/pylibs/pylama/checkers/pylint/logilab/astng/brain/py2stdlib.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/brain/py2stdlib.py rename to pylibs/pylama/checkers/pylint/logilab/astng/brain/py2stdlib.py diff --git a/pylibs/pylama/pylint/logilab/astng/builder.py b/pylibs/pylama/checkers/pylint/logilab/astng/builder.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/builder.py rename to pylibs/pylama/checkers/pylint/logilab/astng/builder.py diff --git a/pylibs/pylama/pylint/logilab/astng/exceptions.py b/pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/exceptions.py rename to pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py diff --git a/pylibs/pylama/pylint/logilab/astng/inference.py b/pylibs/pylama/checkers/pylint/logilab/astng/inference.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/inference.py rename to pylibs/pylama/checkers/pylint/logilab/astng/inference.py diff --git a/pylibs/pylama/pylint/logilab/astng/manager.py b/pylibs/pylama/checkers/pylint/logilab/astng/manager.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/manager.py rename to pylibs/pylama/checkers/pylint/logilab/astng/manager.py diff --git a/pylibs/pylama/pylint/logilab/astng/mixins.py b/pylibs/pylama/checkers/pylint/logilab/astng/mixins.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/mixins.py rename to pylibs/pylama/checkers/pylint/logilab/astng/mixins.py diff --git a/pylibs/pylama/pylint/logilab/astng/node_classes.py b/pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/node_classes.py rename to pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py diff --git a/pylibs/pylama/pylint/logilab/astng/nodes.py b/pylibs/pylama/checkers/pylint/logilab/astng/nodes.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/nodes.py rename to pylibs/pylama/checkers/pylint/logilab/astng/nodes.py diff --git a/pylibs/pylama/pylint/logilab/astng/protocols.py b/pylibs/pylama/checkers/pylint/logilab/astng/protocols.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/protocols.py rename to pylibs/pylama/checkers/pylint/logilab/astng/protocols.py diff --git a/pylibs/pylama/pylint/logilab/astng/raw_building.py b/pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/raw_building.py rename to pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py diff --git a/pylibs/pylama/pylint/logilab/astng/rebuilder.py b/pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/rebuilder.py rename to pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py diff --git a/pylibs/pylama/pylint/logilab/astng/scoped_nodes.py b/pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/scoped_nodes.py rename to pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py diff --git a/pylibs/pylama/pylint/logilab/astng/utils.py b/pylibs/pylama/checkers/pylint/logilab/astng/utils.py similarity index 100% rename from pylibs/pylama/pylint/logilab/astng/utils.py rename to pylibs/pylama/checkers/pylint/logilab/astng/utils.py diff --git a/pylibs/pylama/pylint/logilab/common/__init__.py b/pylibs/pylama/checkers/pylint/logilab/common/__init__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/__init__.py rename to pylibs/pylama/checkers/pylint/logilab/common/__init__.py diff --git a/pylibs/pylama/pylint/logilab/common/__pkginfo__.py b/pylibs/pylama/checkers/pylint/logilab/common/__pkginfo__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/__pkginfo__.py rename to pylibs/pylama/checkers/pylint/logilab/common/__pkginfo__.py diff --git a/pylibs/pylama/pylint/logilab/common/changelog.py b/pylibs/pylama/checkers/pylint/logilab/common/changelog.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/changelog.py rename to pylibs/pylama/checkers/pylint/logilab/common/changelog.py diff --git a/pylibs/pylama/pylint/logilab/common/compat.py b/pylibs/pylama/checkers/pylint/logilab/common/compat.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/compat.py rename to pylibs/pylama/checkers/pylint/logilab/common/compat.py diff --git a/pylibs/pylama/pylint/logilab/common/configuration.py b/pylibs/pylama/checkers/pylint/logilab/common/configuration.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/configuration.py rename to pylibs/pylama/checkers/pylint/logilab/common/configuration.py diff --git a/pylibs/pylama/pylint/logilab/common/decorators.py b/pylibs/pylama/checkers/pylint/logilab/common/decorators.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/decorators.py rename to pylibs/pylama/checkers/pylint/logilab/common/decorators.py diff --git a/pylibs/pylama/pylint/logilab/common/deprecation.py b/pylibs/pylama/checkers/pylint/logilab/common/deprecation.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/deprecation.py rename to pylibs/pylama/checkers/pylint/logilab/common/deprecation.py diff --git a/pylibs/pylama/pylint/logilab/common/graph.py b/pylibs/pylama/checkers/pylint/logilab/common/graph.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/graph.py rename to pylibs/pylama/checkers/pylint/logilab/common/graph.py diff --git a/pylibs/pylama/pylint/logilab/common/interface.py b/pylibs/pylama/checkers/pylint/logilab/common/interface.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/interface.py rename to pylibs/pylama/checkers/pylint/logilab/common/interface.py diff --git a/pylibs/pylama/pylint/logilab/common/modutils.py b/pylibs/pylama/checkers/pylint/logilab/common/modutils.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/modutils.py rename to pylibs/pylama/checkers/pylint/logilab/common/modutils.py diff --git a/pylibs/pylama/pylint/logilab/common/optik_ext.py b/pylibs/pylama/checkers/pylint/logilab/common/optik_ext.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/optik_ext.py rename to pylibs/pylama/checkers/pylint/logilab/common/optik_ext.py diff --git a/pylibs/pylama/pylint/logilab/common/textutils.py b/pylibs/pylama/checkers/pylint/logilab/common/textutils.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/textutils.py rename to pylibs/pylama/checkers/pylint/logilab/common/textutils.py diff --git a/pylibs/pylama/pylint/logilab/common/tree.py b/pylibs/pylama/checkers/pylint/logilab/common/tree.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/tree.py rename to pylibs/pylama/checkers/pylint/logilab/common/tree.py diff --git a/pylibs/pylama/pylint/logilab/common/ureports/__init__.py b/pylibs/pylama/checkers/pylint/logilab/common/ureports/__init__.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/ureports/__init__.py rename to pylibs/pylama/checkers/pylint/logilab/common/ureports/__init__.py diff --git a/pylibs/pylama/pylint/logilab/common/ureports/docbook_writer.py b/pylibs/pylama/checkers/pylint/logilab/common/ureports/docbook_writer.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/ureports/docbook_writer.py rename to pylibs/pylama/checkers/pylint/logilab/common/ureports/docbook_writer.py diff --git a/pylibs/pylama/pylint/logilab/common/ureports/html_writer.py b/pylibs/pylama/checkers/pylint/logilab/common/ureports/html_writer.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/ureports/html_writer.py rename to pylibs/pylama/checkers/pylint/logilab/common/ureports/html_writer.py diff --git a/pylibs/pylama/pylint/logilab/common/ureports/nodes.py b/pylibs/pylama/checkers/pylint/logilab/common/ureports/nodes.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/ureports/nodes.py rename to pylibs/pylama/checkers/pylint/logilab/common/ureports/nodes.py diff --git a/pylibs/pylama/pylint/logilab/common/ureports/text_writer.py b/pylibs/pylama/checkers/pylint/logilab/common/ureports/text_writer.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/ureports/text_writer.py rename to pylibs/pylama/checkers/pylint/logilab/common/ureports/text_writer.py diff --git a/pylibs/pylama/pylint/logilab/common/visitor.py b/pylibs/pylama/checkers/pylint/logilab/common/visitor.py similarity index 100% rename from pylibs/pylama/pylint/logilab/common/visitor.py rename to pylibs/pylama/checkers/pylint/logilab/common/visitor.py diff --git a/pylibs/pylama/pylint/reporters/__init__.py b/pylibs/pylama/checkers/pylint/reporters/__init__.py similarity index 100% rename from pylibs/pylama/pylint/reporters/__init__.py rename to pylibs/pylama/checkers/pylint/reporters/__init__.py diff --git a/pylibs/pylama/pylint/reporters/guireporter.py b/pylibs/pylama/checkers/pylint/reporters/guireporter.py similarity index 100% rename from pylibs/pylama/pylint/reporters/guireporter.py rename to pylibs/pylama/checkers/pylint/reporters/guireporter.py diff --git a/pylibs/pylama/pylint/reporters/html.py b/pylibs/pylama/checkers/pylint/reporters/html.py similarity index 100% rename from pylibs/pylama/pylint/reporters/html.py rename to pylibs/pylama/checkers/pylint/reporters/html.py diff --git a/pylibs/pylama/pylint/reporters/text.py b/pylibs/pylama/checkers/pylint/reporters/text.py similarity index 100% rename from pylibs/pylama/pylint/reporters/text.py rename to pylibs/pylama/checkers/pylint/reporters/text.py diff --git a/pylibs/pylama/pylint/utils.py b/pylibs/pylama/checkers/pylint/utils.py similarity index 100% rename from pylibs/pylama/pylint/utils.py rename to pylibs/pylama/checkers/pylint/utils.py diff --git a/pylibs/pylama/core.py b/pylibs/pylama/core.py index 70915075..0349dd87 100644 --- a/pylibs/pylama/core.py +++ b/pylibs/pylama/core.py @@ -1,4 +1,4 @@ -""" Pylama core. +""" Pylama core functionality. Get params and runs a checkers. """ import logging import re @@ -10,7 +10,7 @@ LOGGER = logging.getLogger('pylama') MODERE = re.compile(r'^\s*#\s+(?:pymode\:)?((?:lint[\w_]*=[^:\n\s]+:?)+)', re.I | re.M) -SKIP_PATTERN = '# nolint' +SKIP_PATTERN = '# noqa' STREAM = logging.StreamHandler() LOGGER.addHandler(STREAM) @@ -32,6 +32,7 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, code = f.read() + '\n\n' params = config or __parse_modeline(code) params['skip'] = [False] + for line in code.split('\n'): params['skip'].append(line.endswith(SKIP_PATTERN)) @@ -82,7 +83,7 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, except Exception: import traceback - logging.error(traceback.format_exc()) + logging.debug(traceback.format_exc()) errors = [er for er in errors if __ignore_error(er, select, ignore)] return sorted(errors, key=lambda x: x['lnum']) diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index fa50899c..846321db 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -1,4 +1,4 @@ -""" SCM hooks. +""" SCM hooks. Integration with git and mercurial. """ from __future__ import absolute_import @@ -10,7 +10,7 @@ try: - from configparser import ConfigParser # nolint + from configparser import ConfigParser # noqa except ImportError: # Python 2 from ConfigParser import ConfigParser @@ -95,11 +95,15 @@ def install_hook(path): git = op.join(path, '.git', 'hooks') hg = op.join(path, '.hg') if op.exists(git): - install_git(git) and LOGGER.warn('Git hook has been installed.') # nolint + install_git(git) + LOGGER.warn('Git hook has been installed.') elif op.exists(hg): - install_hg(git) and LOGGER.warn('Mercurial hook has been installed.') # nolint + install_hg(git) + LOGGER.warn('Mercurial hook has been installed.') else: LOGGER.error('VCS has not found. Check your path.') sys.exit(1) + +# lint_ignore=F0401 diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index 22998b2b..8e665724 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -1,9 +1,15 @@ """ Inirama is a python module that parses INI files. + .. _badges: .. include:: ../README.rst - :start-line: 5 - :end-line: 12 + :start-after: .. _badges: + :end-before: .. _contents: + + .. _description: + .. include:: ../README.rst + :start-after: .. _description: + :end-before: .. _badges: :copyright: 2013 by Kirill Klenov. :license: BSD, see LICENSE for more details. @@ -20,7 +26,7 @@ from ordereddict import OrderedDict -__version__ = '0.4.0' +__version__ = '0.4.1' __project__ = 'Inirama' __author__ = "Kirill Klenov " __license__ = "BSD" @@ -31,11 +37,14 @@ class Scanner(object): + """ Split a code string on tokens. """ + def __init__(self, source, ignore=None, patterns=None): """ Init Scanner instance. - :param patterns: List of token patterns [(token, regexp)] - :param ignore: List of ignored tokens + :param patterns: List of token patterns [(token, regexp)] + :param ignore: List of ignored tokens + """ self.reset(source) if patterns: @@ -47,17 +56,18 @@ def __init__(self, source, ignore=None, patterns=None): self.ignore = ignore def reset(self, source): - """ Reset scanner. + """ Reset scanner's state. + + :param source: Source for parsing - :param source: Source for parsing """ self.tokens = [] self.source = source self.pos = 0 def scan(self): - """ Scan source and grab tokens. - """ + """ Scan source and grab tokens. """ + self.pre_scan() token = None @@ -98,18 +108,23 @@ def scan(self): self.tokens.append(token) def pre_scan(self): - """ Prepare source. - """ + """ Prepare source. """ pass def __repr__(self): - """ Print the last 5 tokens that have been scanned in + """ Print the last 5 tokens that have been scanned in. + + :return str: + """ return '" class INIScanner(Scanner): + + """ Get tokens for INI. """ + patterns = [ ('SECTION', re.compile(r'\[[^]]+\]')), ('IGNORE', re.compile(r'[ \r\t\n]+')), @@ -119,6 +134,7 @@ class INIScanner(Scanner): ignore = ['IGNORE'] def pre_scan(self): + """ Prepare string for scaning. """ escape_re = re.compile(r'\\\n[\t ]+') self.source = escape_re.sub('', self.source) @@ -128,6 +144,8 @@ def pre_scan(self): class Section(MutableMapping): + """ Representation of INI section. """ + def __init__(self, namespace, *args, **kwargs): super(Section, self).__init__(*args, **kwargs) self.namespace = namespace @@ -152,6 +170,7 @@ def __repr__(self): return "<{0} {1}>".format(self.__class__.__name__, str(dict(self))) def iteritems(self): + """ Impletment iteritems. """ for key in self.__storage__.keys(): yield key, self[key] @@ -160,9 +179,17 @@ def iteritems(self): class InterpolationSection(Section): + """ INI section with interpolation support. """ + var_re = re.compile('{([^}]+)}') def get(self, name, default=None): + """ Get item by name. + + :return object: value or None if name not exists + + """ + if name in self: return self[name] return default @@ -189,26 +216,28 @@ def __getitem__(self, name): class Namespace(object): + """ Default class for parsing INI. - :param **default_items: Default items for default section. + :param **default_items: Default items for default section. - Usage - ----- + Usage + ----- - :: + :: - from inirama import Namespace + from inirama import Namespace - ns = Namespace() - ns.read('config.ini') + ns = Namespace() + ns.read('config.ini') - print ns['section']['key'] + print ns['section']['key'] - ns['other']['new'] = 'value' - ns.write('new_config.ini') + ns['other']['new'] = 'value' + ns.write('new_config.ini') """ + #: Name of default section (:attr:`~inirama.Namespace.default`) default_section = 'DEFAULT' @@ -226,16 +255,20 @@ def __init__(self, **default_items): @property def default(self): """ Return default section or empty dict. + + :return :class:`inirama.Section`: section + """ return self.sections.get(self.default_section, dict()) def read(self, *files, **params): """ Read and parse INI files. - :param *files: Files for reading - :param **params: Params for parsing + :param *files: Files for reading + :param **params: Params for parsing + + Set `update=False` for prevent values redefinition. - Set `update=False` for prevent values redefinition. """ for f in files: try: @@ -248,10 +281,10 @@ def read(self, *files, **params): raise def write(self, f): - """ - Write namespace as INI file. + """ Write namespace as INI file. + + :param f: File object or path to file. - :param f: File object or path to file. """ if isinstance(f, str): f = io.open(f, 'w', encoding='utf-8') @@ -270,8 +303,9 @@ def write(self, f): def parse(self, source, update=True, **params): """ Parse INI source as string. - :param source: Source of INI - :param update: Replace alredy defined items + :param source: Source of INI + :param update: Replace alredy defined items + """ scanner = INIScanner(source) scanner.scan() @@ -291,6 +325,9 @@ def parse(self, source, update=True, **params): def __getitem__(self, name): """ Look name in self sections. + + :return :class:`inirama.Section`: section + """ if not name in self.sections: self.sections[name] = self.section_type(self) @@ -304,6 +341,7 @@ def __repr__(self): class InterpolationNamespace(Namespace): + """ That implements the interpolation feature. :: @@ -323,4 +361,4 @@ class InterpolationNamespace(Namespace): section_type = InterpolationSection -# lint_ignore=W0201,R0924 +# lint_ignore=W0201,R0924,F0401 diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index a9cf9473..dfe38ece 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -102,12 +102,14 @@ def shell(): if op.isdir(options.path): paths = [] for root, _, files in walk(options.path): - paths += [op.join(root, f) for f in files if f.endswith('.py')] + paths += [ + op.relpath(op.join(root, f), curdir) + for f in files if f.endswith('.py')] check_files( paths, async=options.async, - rootpath=options.path, + rootpath=curdir, skip=options.skip, frmt=options.format, ignore=options.ignore, @@ -132,12 +134,12 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, params = dict() if config: for key, section in config.sections.items(): - if key != 'main': - params[op.abspath(key)] = prepare_params(section) + if key != config.default_section: + mask = re.compile(fnmatch.translate(key)) + params[mask] = prepare_params(section) work_paths = [] for path in paths: - path = op.abspath(path) if skip and any(pattern.match(path) for pattern in skip): LOGGER.info('Skip path: %s', path) continue @@ -164,9 +166,5 @@ def prepare_params(section): return params -def __parse_options(args=None): - pass - - if __name__ == '__main__': shell() diff --git a/pylibs/pylama/tasks.py b/pylibs/pylama/tasks.py index 3d301c04..cf934477 100644 --- a/pylibs/pylama/tasks.py +++ b/pylibs/pylama/tasks.py @@ -91,10 +91,16 @@ def check_path(path, rootpath='.', ignore=None, select=None, linters=None, LOGGER.info("Parse file: %s", path) params = params or dict() + config = dict() + + for mask in params: + if mask.match(path): + config.update(params[mask]) errors = [] - for error in run(path, ignore=ignore, select=select, linters=linters, - complexity=complexity, config=params.get(path)): + for error in run( + path, ignore=ignore, select=select, linters=linters, + complexity=complexity, config=config): try: error['rel'] = op.relpath( error['filename'], op.dirname(rootpath)) diff --git a/pylibs/pylama/utils.py b/pylibs/pylama/utils.py index 2a135cf8..022b535d 100644 --- a/pylibs/pylama/utils.py +++ b/pylibs/pylama/utils.py @@ -5,7 +5,7 @@ import _ast from os import path as op, environ -from .pep8 import BaseReport, StyleGuide +from .checkers.pep8 import BaseReport, StyleGuide __all__ = 'pep8', 'pep257', 'mccabe', 'pyflakes', 'pylint' @@ -62,7 +62,7 @@ def mccabe(path, code=None, complexity=8, **meta): :return list: List of errors. """ - from .mccabe import get_code_complexity + from .checkers.mccabe import get_code_complexity return get_code_complexity(code, complexity, filename=path) or [] @@ -73,7 +73,7 @@ def pyflakes(path, code=None, **meta): :return list: List of errors. """ - from .pyflakes import checker + from .checkers.pyflakes import checker errors = [] tree = compile(code, path, "exec", _ast.PyCF_ONLY_AST) @@ -99,9 +99,9 @@ def pylint(path, **meta): logging.warn("Pylint don't supported python3 and will be disabled.") return [] - from .pylint.lint import Run - from .pylint.reporters import BaseReporter - from .pylint.logilab.astng import MANAGER + from .checkers.pylint.lint import Run + from .checkers.pylint.reporters import BaseReporter + from .checkers.pylint.logilab.astng import MANAGER MANAGER.astng_cache.clear() @@ -141,7 +141,7 @@ def pep257(path, **meta): """ f = open(path) - from .pep257 import check_source + from .checkers.pep257 import check_source errors = [] for er in check_source(f.read(), path): diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index 0f315c8a..96a7134a 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -1,12 +1,15 @@ +""" Pylama support. """ from __future__ import absolute_import -import locale +import fnmatch import json +import locale +from os import path as op +from re import compile as re from pylama.core import run -from pylama.main import prepare_params from pylama.inirama import Namespace -from os import path as op +from pylama.main import prepare_params from . import interface from .queue import add_task @@ -19,6 +22,7 @@ def check_file(): + """ Check current buffer. """ checkers = interface.get_option('lint_checker').split(',') buf = interface.get_current_buffer() @@ -46,8 +50,13 @@ def check_file(): complexity = int(interface.get_option('lint_mccabe_complexity') or 0) - params = None + params = dict() relpath = op.relpath(buf.name, curdir) + for mask in config.sections: + mask_re = re(fnmatch.translate(mask)) + if mask_re.match(relpath): + params.update(prepare_params(config[mask])) + if relpath in config: params = prepare_params(config[relpath]) @@ -64,7 +73,11 @@ def check_file(): def run_checkers(checkers=None, ignore=None, buf=None, select=None, complexity=None, callback=None, config=None): + """ Run pylama code. + + :return list: errors + """ pylint_options = '--rcfile={0} -r n'.format( interface.get_var('lint_config')).split() @@ -74,6 +87,7 @@ def run_checkers(checkers=None, ignore=None, buf=None, select=None, def parse_result(result, buf=None, **kwargs): + """ Parse results. """ interface.command('let g:qf_list = ' + json.dumps(result)) interface.command('call pymode#lint#Parse({0})'.format(buf.number)) From 2ec13e2991e905ae4c30d1fcc09312a1bff62e1b Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Sun, 23 Jun 2013 14:01:02 +0800 Subject: [PATCH 034/582] Fix autopep --- pylibs/autopep8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylibs/autopep8.py b/pylibs/autopep8.py index 0cef7623..aa211dc4 100644 --- a/pylibs/autopep8.py +++ b/pylibs/autopep8.py @@ -54,7 +54,7 @@ import difflib import tempfile -from pylama import pep8 +from pylama.checkers import pep8 try: From 82b769d45284b66d5d7aae9b04235804da816ec4 Mon Sep 17 00:00:00 2001 From: Matt Stevens Date: Tue, 2 Jul 2013 01:35:48 +0100 Subject: [PATCH 035/582] Remove trailing whitespace from README and docs --- README.rst | 4 ++-- doc/pymode.txt | 28 ++++++++++++++-------------- doc/ropevim.txt | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index a8f67c24..cf930d06 100644 --- a/README.rst +++ b/README.rst @@ -191,7 +191,7 @@ Default values: :: let g:pymode_lint_maxheight = 6 -.. note:: +.. note:: Pylint options (ex. disable messages) may be defined in ``$HOME/pylint.rc`` See pylint documentation: http://pylint-messages.wikidot.com/all-codes @@ -511,7 +511,7 @@ License Licensed under a `GNU lesser general public license`_. -If you like this plugin, you can send me postcard :) +If you like this plugin, you can send me postcard :) My address is here: "Russia, 143401, Krasnogorsk, Shkolnaya 1-19" to "Kirill Klenov". **Thanks for support!** diff --git a/doc/pymode.txt b/doc/pymode.txt index 01c150f6..202b420c 100644 --- a/doc/pymode.txt +++ b/doc/pymode.txt @@ -104,13 +104,13 @@ PythonMode. These options should be set in your vimrc. |'pymode_syntax_print_as_function'| Hightlight `print` as function -|'pymode_syntax_highlight_equal_operator'| Hightlight `=` +|'pymode_syntax_highlight_equal_operator'| Hightlight `=` -|'pymode_syntax_highlight_stars_operator'| Hightlight `*` +|'pymode_syntax_highlight_stars_operator'| Hightlight `*` -|'pymode_syntax_highlight_self'| Hightlight `self` +|'pymode_syntax_highlight_self'| Hightlight `self` -|'pymode_syntax_indent_errors'| Hightlight indentation errors +|'pymode_syntax_indent_errors'| Hightlight indentation errors |'pymode_syntax_space_errors'| Hightlight trailing spaces as errors @@ -152,7 +152,7 @@ To enable any of the options below you should put the given line in your 2.2. Modeline ~ *PythonModeModeline* -The VIM modeline `:help modeline` feature allows you to change pymode +The VIM modeline `:help modeline` feature allows you to change pymode options for the current file. Pymode modeline should always be the last line in the vimrc file and look like: @@ -393,28 +393,28 @@ Hightlight `print` as function Values: 0 or 1. Default: |'pymode_syntax_all'|. -Hightlight `=` +Hightlight `=` ------------------------------------------------------------------------------ *'pymode_syntax_highlight_stars_operator'* Values: 0 or 1. Default: |'pymode_syntax_all'|. -Hightlight `*` +Hightlight `*` ------------------------------------------------------------------------------ *'pymode_syntax_highlight_self'* Values: 0 or 1. Default: |'pymode_syntax_all'|. -Hightlight `self` +Hightlight `self` ------------------------------------------------------------------------------ *'pymode_syntax_indent_errors'* Values: 0 or 1. Default: |'pymode_syntax_all'|. -Hightlight indentation errors +Hightlight indentation errors ------------------------------------------------------------------------------ *'pymode_syntax_space_errors'* @@ -576,7 +576,7 @@ iM Operation with inner function or method. *:PyLintAuto* *PyLintAuto* Automatically fix PEP8 errors in the current buffer - + *:Pyrun* *Pyrun* Run current buffer @@ -599,7 +599,7 @@ To work, rope_ creates a service directory: `.ropeproject`. If |'pymode_rope_guess_project'| is set on (as it is by default) and `.ropeproject` is not found in the current dir, rope will scan for `.ropeproject` in every dir in the parent path. If rope finds `.ropeproject` -in parent dirs, rope sets projectis for all child dirs and the scan may be +in parent dirs, rope sets projectis for all child dirs and the scan may be slow for many dirs and files. Solutions: @@ -621,7 +621,7 @@ modules if possible. Try using pyflakes: see |'pymode_lint_checker'|. You may set |exrc| and |secure| in your |vimrc| to auto-set custom settings from `.vimrc` from your projects directories. > - Example: On Flask projects I automatically set + Example: On Flask projects I automatically set 'g:pymode_lint_checker = "pyflakes"'. On Django 'g:pymode_lint_checker = "pylint"' < @@ -643,7 +643,7 @@ The sequence of commands that fixed this: ============================================================================== 6. Credits ~ - *PythonModeCredits* + *PythonModeCredits* Kirill Klenov http://klen.github.com/ http://github.com/klen/ @@ -684,7 +684,7 @@ The sequence of commands that fixed this: Python-mode is released under the GNU lesser general public license. See: http://www.gnu.org/copyleft/lesser.html -If you like this plugin, you can send me a postcard :) +If you like this plugin, you can send me a postcard :) My address is: "Russia, 143401, Krasnogorsk, Shkolnaya 1-19" to "Kirill Klenov". Thanks for your support! diff --git a/doc/ropevim.txt b/doc/ropevim.txt index 94a76216..0bf13bb8 100644 --- a/doc/ropevim.txt +++ b/doc/ropevim.txt @@ -151,7 +151,7 @@ those under ``ropetest`` folder. The find occurrences command (" f" by default) can be used to find the occurrences of a python name. If ``unsure`` option is ``yes``, it will also show unsure occurrences; unsure occurrences are -indicated with a ``?`` mark in the end. +indicated with a ``?`` mark in the end. Note: That ropevim uses the quickfix feature of vim for @@ -183,7 +183,7 @@ values you can use: > name2 line3 < -Each line of the definition should start with a space or a tab. +Each line of the definition should start with a space or a tab. Note: That blank lines before the name of config definitions are ignored. @@ -222,7 +222,7 @@ restructuring can be: > *RopeVariables* *'pymode_rope_codeassist_maxfixes'* The maximum number of syntax errors - to fix for code assists. + to fix for code assists. The default value is `1`. *'pymode_rope_local_prefix'* The prefix for ropevim refactorings. From 9756409fcb0697a342a3cf73fa464aedbf6c0b58 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Mon, 8 Jul 2013 16:17:03 +0800 Subject: [PATCH 036/582] Update pylama to version 1.3.0 --- pylibs/pylama/__init__.py | 2 +- pylibs/pylama/checkers/pep8.py | 27 +++-- pylibs/pylama/checkers/pyflakes/checker.py | 47 ++++---- pylibs/pylama/core.py | 131 +++++++++++++-------- pylibs/pylama/hook.py | 21 ++-- pylibs/pylama/main.py | 35 ++++-- pylibs/pylama/tasks.py | 3 +- 7 files changed, 163 insertions(+), 103 deletions(-) diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 4cc579fc..acbcf35b 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -5,7 +5,7 @@ :license: BSD, see LICENSE for more details. """ -version_info = 1, 1, 0 +version_info = 1, 3, 0 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/checkers/pep8.py b/pylibs/pylama/checkers/pep8.py index 602466d9..8413270f 100644 --- a/pylibs/pylama/checkers/pep8.py +++ b/pylibs/pylama/checkers/pep8.py @@ -45,7 +45,7 @@ 700 statements 900 syntax error """ -__version__ = '1.4.6a0' +__version__ = '1.4.6' import os import sys @@ -63,7 +63,7 @@ from ConfigParser import RawConfigParser DEFAULT_EXCLUDE = '.svn,CVS,.bzr,.hg,.git,__pycache__' -DEFAULT_IGNORE = 'E226,E24' +DEFAULT_IGNORE = 'E123,E226,E24' if sys.platform == 'win32': DEFAULT_CONFIG = os.path.expanduser(r'~\.pep8') else: @@ -381,7 +381,8 @@ def indentation(logical_line, previous_logical, indent_char, yield 0, "E113 unexpected indentation" -def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): +def continued_indentation(logical_line, tokens, indent_level, hang_closing, + noqa, verbose): r""" Continuation lines should align wrapped elements either vertically using Python's implicit line joining inside parentheses, brackets and braces, or @@ -458,7 +459,8 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): open_row = 0 hang = rel_indent[row] - rel_indent[open_row] close_bracket = (token_type == tokenize.OP and text in ']})') - visual_indent = not close_bracket and indent_chances.get(start[1]) + visual_indent = (not close_bracket and hang > 0 and + indent_chances.get(start[1])) if close_bracket and indent[depth]: # closing bracket for visual indent @@ -467,7 +469,8 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): "visual indentation") elif close_bracket and not hang: # closing bracket matches indentation of opening bracket's line - pass + if hang_closing: + yield start, "E133 closing bracket is missing indentation" elif visual_indent is True: # visual indent is verified if not indent[depth]: @@ -481,7 +484,7 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): "under-indented for visual indent") elif hang == 4 or (indent_next and rel_indent[row] == 8): # hanging indent is verified - if close_bracket: + if close_bracket and not hang_closing: yield (start, "E123 closing bracket does not match " "indentation of opening bracket's line") else: @@ -535,6 +538,7 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): for idx in range(row, -1, -1): if parens[idx]: parens[idx] -= 1 + rel_indent[row] = rel_indent[idx] break assert len(indent) == depth + 1 if start[1] not in indent_chances: @@ -543,7 +547,7 @@ def continued_indentation(logical_line, tokens, indent_level, noqa, verbose): last_token_multiline = (start[0] != end[0]) - if indent_next and rel_indent[-1] == 4: + if indent_next and expand_indent(line) == indent_level + 4: yield (last_indent, "E125 continuation line does not distinguish " "itself from next logical line") @@ -1183,6 +1187,7 @@ def __init__(self, filename=None, lines=None, self._logical_checks = options.logical_checks self._ast_checks = options.ast_checks self.max_line_length = options.max_line_length + self.hang_closing = options.hang_closing self.verbose = options.verbose self.filename = filename if filename is None: @@ -1693,8 +1698,9 @@ def get_parser(prog='pep8', version=__version__): parser = OptionParser(prog=prog, version=version, usage="%prog [options] input ...") parser.config_options = [ - 'exclude', 'filename', 'select', 'ignore', 'max-line-length', 'count', - 'format', 'quiet', 'show-pep8', 'show-source', 'statistics', 'verbose'] + 'exclude', 'filename', 'select', 'ignore', 'max-line-length', + 'hang-closing', 'count', 'format', 'quiet', 'show-pep8', + 'show-source', 'statistics', 'verbose'] 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', @@ -1729,6 +1735,9 @@ def get_parser(prog='pep8', version=__version__): default=MAX_LINE_LENGTH, help="set maximum allowed line length " "(default: %default)") + parser.add_option('--hang-closing', action='store_true', + help="hang closing bracket instead of matching " + "indentation of opening bracket's line") parser.add_option('--format', metavar='format', default='default', help="set the error format [default|pylint|]") parser.add_option('--diff', action='store_true', diff --git a/pylibs/pylama/checkers/pyflakes/checker.py b/pylibs/pylama/checkers/pyflakes/checker.py index 3d4c6475..272caa6d 100644 --- a/pylibs/pylama/checkers/pyflakes/checker.py +++ b/pylibs/pylama/checkers/pyflakes/checker.py @@ -192,6 +192,10 @@ def unusedAssignments(self): yield name, binding +class GeneratorScope(Scope): + pass + + class ModuleScope(Scope): pass @@ -319,11 +323,14 @@ def checkDeadScopes(self): self.report(messages.UnusedImport, importation.source, importation.name) - def pushFunctionScope(self): - self.scopeStack.append(FunctionScope()) + def pushScope(self, scopeClass=FunctionScope): + self.scopeStack.append(scopeClass()) + + def pushFunctionScope(self): # XXX Deprecated + self.pushScope(FunctionScope) - def pushClassScope(self): - self.scopeStack.append(ClassScope()) + def pushClassScope(self): # XXX Deprecated + self.pushScope(ClassScope) def report(self, messageClass, *args, **kwargs): self.messages.append(messageClass(self.filename, *args, **kwargs)) @@ -437,12 +444,15 @@ def handleNodeLoad(self, node): else: return - # try enclosing function scopes + scopes = [scope for scope in self.scopeStack[:-1] + if isinstance(scope, (FunctionScope, ModuleScope))] + if isinstance(self.scope, GeneratorScope) and scopes[-1] != self.scopeStack[-2]: + scopes.append(self.scopeStack[-2]) + + # try enclosing function scopes and global scope importStarred = self.scope.importStarred - for scope in self.scopeStack[-2:0:-1]: + for scope in reversed(scopes): importStarred = importStarred or scope.importStarred - if not isinstance(scope, FunctionScope): - continue try: scope[name].used = (self.scope, node) except KeyError: @@ -450,15 +460,6 @@ def handleNodeLoad(self, node): else: return - # try global scope - importStarred = importStarred or self.scopeStack[0].importStarred - try: - self.scopeStack[0][name].used = (self.scope, node) - except KeyError: - pass - else: - return - # look in the built-ins if importStarred or name in self.builtIns: return @@ -570,7 +571,7 @@ def handleDoctests(self, node): # leading whitespace: ... return node_offset = self.offset or (0, 0) - self.pushFunctionScope() + self.pushScope() for example in examples: try: tree = compile(example.source, "", "exec", ast.PyCF_ONLY_AST) @@ -632,7 +633,7 @@ def LISTCOMP(self, node): self.handleNode(node.elt, node) def GENERATOREXP(self, node): - self.pushFunctionScope() + self.pushScope(GeneratorScope) # handle generators before element for gen in node.generators: self.handleNode(gen, node) @@ -642,7 +643,7 @@ def GENERATOREXP(self, node): SETCOMP = GENERATOREXP def DICTCOMP(self, node): - self.pushFunctionScope() + self.pushScope(GeneratorScope) for gen in node.generators: self.handleNode(gen, node) self.handleNode(node.key, node) @@ -742,7 +743,7 @@ def addArgs(arglist): def runFunction(): - self.pushFunctionScope() + self.pushScope() for name in args: self.addBinding(node, Argument(name, node), reportRedef=False) if isinstance(node.body, list): @@ -777,7 +778,7 @@ def CLASSDEF(self, node): if not PY2: for keywordNode in node.keywords: self.handleNode(keywordNode, node) - self.pushClassScope() + self.pushScope(ClassScope) if self.withDoctest: self.deferFunction(lambda: self.handleDoctests(node)) for stmt in node.body: @@ -847,5 +848,3 @@ def EXCEPTHANDLER(self, node): if isinstance(node.name, str): self.handleNodeStore(node) self.handleChildren(node) - -# pymode:lint=0 diff --git a/pylibs/pylama/core.py b/pylibs/pylama/core.py index 0349dd87..8dddc4e1 100644 --- a/pylibs/pylama/core.py +++ b/pylibs/pylama/core.py @@ -1,4 +1,4 @@ -""" Pylama core functionality. Get params and runs a checkers. +""" Pylama core functionality. Get params and runs the checkers. """ import logging import re @@ -6,13 +6,19 @@ from . import utils +#: A default checkers DEFAULT_LINTERS = 'pep8', 'pyflakes', 'mccabe' -LOGGER = logging.getLogger('pylama') -MODERE = re.compile(r'^\s*#\s+(?:pymode\:)?((?:lint[\w_]*=[^:\n\s]+:?)+)', - re.I | re.M) + +#: The skip pattern SKIP_PATTERN = '# noqa' -STREAM = logging.StreamHandler() +# Parse a modelines +MODELINE_RE = re.compile( + r'^\s*#\s+(?:pymode\:)?((?:lint[\w_]*=[^:\n\s]+:?)+)', re.I | re.M) + +# Setup a logger +LOGGER = logging.getLogger('pylama') +STREAM = logging.StreamHandler() LOGGER.addHandler(STREAM) @@ -24,47 +30,43 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, """ errors = [] - ignore = ignore and list(ignore) or [] - select = select and list(select) or [] try: with open(path, 'rU') as f: code = f.read() + '\n\n' - params = config or __parse_modeline(code) - params['skip'] = [False] + + params = prepare_params( + parse_modeline(code), config, ignore=ignore, select=select + ) for line in code.split('\n'): params['skip'].append(line.endswith(SKIP_PATTERN)) - if params.get('lint_ignore'): - ignore += params.get('lint_ignore').split(',') - - if params.get('lint_select'): - select += params.get('lint_select').split(',') - - if params.get('lint'): - for lint in linters: + if not params['lint']: + return errors + + for lint in linters: + try: + linter = getattr(utils, lint) + except AttributeError: + LOGGER.warning("Linter `%s` not found.", lint) + continue + + result = linter(path, code=code, **meta) + for e in result: + e['col'] = e.get('col') or 0 + e['lnum'] = e.get('lnum') or 0 + e['type'] = e.get('type') or 'E' + e['text'] = "{0} [{1}]".format((e.get( + 'text') or '').strip() + .replace("'", "\"").split('\n')[0], lint) + e['filename'] = path or '' try: - linter = getattr(utils, lint) - except AttributeError: - LOGGER.warning("Linter `%s` not found.", lint) + if not params['skip'][e['lnum']]: + errors.append(e) + except IndexError: continue - result = linter(path, code=code, **meta) - for e in result: - e['col'] = e.get('col') or 0 - e['lnum'] = e.get('lnum') or 0 - e['type'] = e.get('type') or 'E' - e['text'] = "{0} [{1}]".format((e.get( - 'text') or '').strip() - .replace("'", "\"").split('\n')[0], lint) - e['filename'] = path or '' - try: - if not params['skip'][e['lnum']]: - errors.append(e) - except IndexError: - continue - except IOError as e: errors.append(dict( lnum=0, @@ -85,29 +87,62 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, import traceback logging.debug(traceback.format_exc()) - errors = [er for er in errors if __ignore_error(er, select, ignore)] + errors = [er for er in errors if filter_errors(er, **params)] return sorted(errors, key=lambda x: x['lnum']) -def __parse_modeline(code): +def parse_modeline(code): """ Parse modeline params from file. :return dict: Linter params. """ - seek = MODERE.search(code) - params = dict(lint=1) + seek = MODELINE_RE.search(code) if seek: - params = dict(v.split('=') for v in seek.group(1).split(':')) - params['lint'] = int(params.get('lint', 1)) + return dict(v.split('=') for v in seek.group(1).split(':')) + + return dict() + + +def prepare_params(*configs, **params): + """ Prepare and merge a params from modeline or config. + + :return dict: + + """ + params['ignore'] = params.get('ignore') or [] + params['select'] = params.get('select') or [] + + for config in filter(None, configs): + for key in ('ignore', 'select'): + config.setdefault(key, config.get('lint_' + key, [])) + if not isinstance(config[key], list): + config[key] = config[key].split(',') + params[key] += config[key] + params['lint'] = config.get('lint', 1) + + params['ignore'] = set(params['ignore']) + params['select'] = set(params['select']) + params['skip'] = [False] + params.setdefault('lint', 1) return params -def __ignore_error(e, select, ignore): - for s in select: - if e['text'].startswith(s): - return True - for i in ignore: - if e['text'].startswith(i): - return False +def filter_errors(e, select=None, ignore=None, **params): + """ Filter a erros by select and ignore lists. + + :return bool: + + """ + + if select: + for s in select: + if e['text'].startswith(s): + return True + + if ignore: + for s in ignore: + if e['text'].startswith(s): + return False + return True diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index 846321db..0f3bddad 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -36,20 +36,21 @@ def git_hook(): check_files([f for f in map(str, files_modified) if f.endswith('.py')]) -def hg_hook(ui, repo, **kwargs): +def hg_hook(ui, repo, node=None, **kwargs): """ Run pylama after mercurial commit. """ from .main import check_files seen = set() paths = [] - for rev in range(repo[kwargs['node']], len(repo)): - for file_ in repo[rev].files(): - file_ = op.join(repo.root, file_) - if file_ in seen or not op.exists(file_): - continue - seen.add(file_) - if file_.endswith('.py'): - paths.append(file_) + if len(repo): + for rev in range(repo[node], len(repo)): + for file_ in repo[rev].files(): + file_ = op.join(repo.root, file_) + if file_ in seen or not op.exists(file_): + continue + seen.add(file_) + if file_.endswith('.py'): + paths.append(file_) LOGGER.setLevel('WARN') check_files(paths) @@ -57,7 +58,7 @@ def hg_hook(ui, repo, **kwargs): def install_git(path): """ Install hook in Git repository. """ hook = op.join(path, 'pre-commit') - with open(hook, 'w+') as fd: + with open(hook, 'w') as fd: fd.write("""#!/usr/bin/env python import sys from pylama.hook import git_hook diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index dfe38ece..4b449746 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -16,9 +16,17 @@ DEFAULT_COMPLEXITY = 10 -def shell(): - """ Endpoint for console. """ +def shell(args=None, error=True): + """ Endpoint for console. + + :return list: list of errors + :raise SystemExit: + + """ curdir = getcwd() + if args is None: + args = sys.argv[1:] + parser = ArgumentParser(description="Code audit tool for python.") parser.add_argument("path", nargs='?', default=curdir, help="Path on file or directory.") @@ -64,7 +72,7 @@ def shell(): parser.add_argument( "--options", "-o", default=op.join(curdir, 'pylama.ini'), help="Select configuration file. By default is '/pylama.ini'") - options = parser.parse_args() + options = parser.parse_args(args) actions = dict((a.dest, a) for a in parser._actions) # Read options from configuration file @@ -106,7 +114,7 @@ def shell(): op.relpath(op.join(root, f), curdir) for f in files if f.endswith('.py')] - check_files( + return check_files( paths, async=options.async, rootpath=curdir, @@ -117,13 +125,19 @@ def shell(): linters=options.linters, complexity=options.complexity, config=config, + error=error, ) def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, select=None, ignore=None, linters=DEFAULT_LINTERS, - complexity=DEFAULT_COMPLEXITY, config=None): - """ Check files. """ + complexity=DEFAULT_COMPLEXITY, config=None, error=True): + """ Check files. + + :return list: list of errors + :raise SystemExit: + + """ from .tasks import async_check_files rootpath = rootpath or getcwd() @@ -149,10 +163,13 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, work_paths, async=async, rootpath=rootpath, ignore=ignore, select=select, linters=linters, complexity=complexity, params=params) - for error in errors: - LOGGER.warning(pattern, error) + for er in errors: + LOGGER.warning(pattern, er) + + if error: + sys.exit(int(bool(errors))) - sys.exit(int(bool(errors))) + return errors def prepare_params(section): diff --git a/pylibs/pylama/tasks.py b/pylibs/pylama/tasks.py index cf934477..485b8905 100644 --- a/pylibs/pylama/tasks.py +++ b/pylibs/pylama/tasks.py @@ -102,8 +102,7 @@ def check_path(path, rootpath='.', ignore=None, select=None, linters=None, path, ignore=ignore, select=select, linters=linters, complexity=complexity, config=config): try: - error['rel'] = op.relpath( - error['filename'], op.dirname(rootpath)) + error['rel'] = op.relpath(error['filename'], rootpath) error['col'] = error.get('col', 1) errors.append(error) except KeyError: From 0910d5d10ac047f29931d15f374e7a5324e22d18 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Mon, 8 Jul 2013 16:33:42 +0800 Subject: [PATCH 037/582] Use pudb if available. --- ftplugin/python/init-pymode.vim | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 59e287ea..529fc9a0 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -155,14 +155,19 @@ endif if !pymode#Default("g:pymode_breakpoint", 1) || g:pymode_breakpoint - if !pymode#Default("g:pymode_breakpoint_cmd", "import ipdb; ipdb.set_trace() # XXX BREAKPOINT") && has("python") + if !pymode#Default("g:pymode_breakpoint_cmd", "import pdb; pdb.set_trace() # XXX BREAKPOINT") && has("python") + python << EOF from imp import find_module -try: - find_module('ipdb') -except ImportError: - vim.command('let g:pymode_breakpoint_cmd = "import pdb; pdb.set_trace() # XXX BREAKPOINT"') + +for module in ('pudb', 'ipdb'): + try: + find_module(module) + vim.command('let g:pymode_breakpoint_cmd = "import %s; %s.set_trace() # XXX BREAKPOINT"' % (module, module)) + except ImportError: + continue EOF + endif " OPTION: g:pymode_breakpoint_key -- string. Key for set/unset breakpoint. From 4fdae4648959aa29bd1a649a7197be424d10e0b6 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Mon, 8 Jul 2013 16:34:16 +0800 Subject: [PATCH 038/582] Update changelog. --- Changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog.rst b/Changelog.rst index a212925e..5536794b 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -3,7 +3,8 @@ Changelog * Added `g:pymode_rope_autocomplete_map` option; * Removed `g:pymode_rope_map_space` option; -* Added PEP257 checker +* Added PEP257 checker; +* Support 'pudb' in breakpoints; ## 2013-05-15 0.6.18 -------------------- From 6ce0fe1d7b128f832a69240fbcda9ed150ecca11 Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Mon, 8 Jul 2013 18:01:04 +0800 Subject: [PATCH 039/582] Fix pylama --- pylibs/pylama/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pylibs/pylama/core.py b/pylibs/pylama/core.py index 8dddc4e1..33d5e239 100644 --- a/pylibs/pylama/core.py +++ b/pylibs/pylama/core.py @@ -110,8 +110,8 @@ def prepare_params(*configs, **params): :return dict: """ - params['ignore'] = params.get('ignore') or [] - params['select'] = params.get('select') or [] + params['ignore'] = list(params.get('ignore') or []) + params['select'] = list(params.get('select') or []) for config in filter(None, configs): for key in ('ignore', 'select'): From 63d92714b5f3e9f7c1985b26da9361abea083edd Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Tue, 30 Jul 2013 10:47:15 +0800 Subject: [PATCH 040/582] Update pylama --- pylibs/pylama/__init__.py | 2 +- pylibs/pylama/config.py | 174 ++++++++++++++++++++++++++++++++++++++ pylibs/pylama/core.py | 12 +-- pylibs/pylama/hook.py | 15 +++- pylibs/pylama/inirama.py | 10 --- pylibs/pylama/main.py | 163 +++++++---------------------------- pylibs/pylama/tasks.py | 24 +++--- pylibs/pymode/lint.py | 65 +++----------- 8 files changed, 247 insertions(+), 218 deletions(-) create mode 100644 pylibs/pylama/config.py diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index acbcf35b..3d7ddc52 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -5,7 +5,7 @@ :license: BSD, see LICENSE for more details. """ -version_info = 1, 3, 0 +version_info = 1, 3, 3 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/config.py b/pylibs/pylama/config.py new file mode 100644 index 00000000..78933872 --- /dev/null +++ b/pylibs/pylama/config.py @@ -0,0 +1,174 @@ +""" Parse arguments from command line and configuration files. """ +import fnmatch +import logging +from argparse import ArgumentParser, Namespace as Options +from os import getcwd, path +from re import compile as re + +from . import version, utils +from .core import DEFAULT_LINTERS, LOGGER, STREAM +from .inirama import Namespace + + +DEFAULT_COMPLEXITY = 10 +CURDIR = getcwd() +DEFAULT_INI_PATH = path.join(CURDIR, 'pylama.ini') + + +def parse_options( + args=None, async=False, select='', ignore='', linters=DEFAULT_LINTERS, + complexity=DEFAULT_COMPLEXITY, options=DEFAULT_INI_PATH): + """ Parse options from command line and configuration files. + + :return argparse.Namespace: + + """ + parser = get_parser() + actions = dict((a.dest, a) for a in parser._actions) + options = Options( + async=_Default(async), format=_Default('pep8'), + select=_Default(select), ignore=_Default(ignore), + report=_Default(None), verbose=_Default(False), + linters=_Default(linters), complexity=_Default(complexity), + options=_Default(options)) + + if not (args is None): + options = parser.parse_args(args) + + config = get_config(str(options.options)) + + for k, v in config.default.items(): + value = getattr(options, k, _Default(None)) + if not isinstance(value, _Default): + continue + + action = actions.get(k) + LOGGER.info('Find option %s (%s)', k, v) + name, value = action.dest, action.type(v)\ + if callable(action.type) else v + if action.const: + value = bool(int(value)) + setattr(options, name, value) + + opts = dict(options.__dict__.items()) + for name, value in opts.items(): + if isinstance(value, _Default): + action = actions.get(name) + if action and callable(action.type): + value.value = action.type(value.value) + + setattr(options, name, value.value) + + options.file_params = dict() + for k, s in config.sections.items(): + if k != config.default_section: + mask = re(fnmatch.translate(k)) + options.file_params[mask] = dict(s) + options.file_params[mask]['lint'] = int( + options.file_params[mask].get('lint', 1) + ) + + return options + + +def setup_logger(options): + """ Setup logger with options. """ + + LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) + if options.report: + LOGGER.removeHandler(STREAM) + LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) + LOGGER.info('Try to read configuration from: ' + options.options) + + +def get_parser(): + """ Make command parser for pylama. + + :return ArgumentParser: + + """ + split_csp_str = lambda s: list( + set(i for i in s.strip().split(',') if i)) + + parser = ArgumentParser(description="Code audit tool for python.") + parser.add_argument( + "path", nargs='?', default=_Default(CURDIR), + help="Path on file or directory.") + + parser.add_argument( + "--verbose", "-v", action='store_true', help="Verbose mode.") + + parser.add_argument('--version', action='version', + version='%(prog)s ' + version) + + parser.add_argument( + "--format", "-f", default=_Default('pep8'), choices=['pep8', 'pylint'], + help="Error format.") + + parser.add_argument( + "--select", "-s", default=_Default(''), type=split_csp_str, + help="Select errors and warnings. (comma-separated)") + + parser.add_argument( + "--linters", "-l", default=_Default(','.join(DEFAULT_LINTERS)), + type=split_csp_str, + help=( + "Select linters. (comma-separated). Choices are %s." + % ','.join(s for s in utils.__all__) + )) + + parser.add_argument( + "--ignore", "-i", default=_Default(''), type=split_csp_str, + help="Ignore errors and warnings. (comma-separated)") + + parser.add_argument( + "--skip", default=_Default(''), + type=lambda s: [re(fnmatch.translate(p)) for p in s.split(',') if p], + help="Skip files by masks (comma-separated, Ex. */messages.py)") + + parser.add_argument( + "--complexity", "-c", default=_Default(DEFAULT_COMPLEXITY), type=int, + help="Set mccabe complexity.") + + parser.add_argument("--report", "-r", help="Filename for report.") + parser.add_argument( + "--hook", action="store_true", help="Install Git (Mercurial) hook.") + + parser.add_argument( + "--async", action="store_true", + help="Enable async mode. Usefull for checking a lot of files. " + "Dont supported with pylint.") + + parser.add_argument( + "--options", "-o", default=_Default(DEFAULT_INI_PATH), + help="Select configuration file. By default is '/pylama.ini'") + + return parser + + +def get_config(ini_path=DEFAULT_INI_PATH): + """ Load configuration from INI. + + :return Namespace: + + """ + config = Namespace() + config.default_section = 'main' + config.read(ini_path) + + return config + + +class _Default(object): + + def __init__(self, value): + self.value = value + + def __getattr__(self, name): + return getattr(self.value, name) + + def __str__(self): + return str(self.value) + + +# lint_ignore=R0914,W0212,E1103,C901 diff --git a/pylibs/pylama/core.py b/pylibs/pylama/core.py index 33d5e239..81620a13 100644 --- a/pylibs/pylama/core.py +++ b/pylibs/pylama/core.py @@ -1,4 +1,5 @@ -""" Pylama core functionality. Get params and runs the checkers. +""" Pylama's core functionality. Prepare params, check a modeline and run the + checkers. """ import logging import re @@ -24,12 +25,13 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, **meta): - """ Run code checking for path. + """ Run a code checkers with given params. :return errors: list of dictionaries with error's information """ errors = [] + params = dict(ignore=ignore, select=select) try: with open(path, 'rU') as f: @@ -92,7 +94,7 @@ def run(path, ignore=None, select=None, linters=DEFAULT_LINTERS, config=None, def parse_modeline(code): - """ Parse modeline params from file. + """ Parse params from file's modeline. :return dict: Linter params. @@ -105,7 +107,7 @@ def parse_modeline(code): def prepare_params(*configs, **params): - """ Prepare and merge a params from modeline or config. + """ Prepare and merge a params from modelines and configs. :return dict: @@ -129,7 +131,7 @@ def prepare_params(*configs, **params): def filter_errors(e, select=None, ignore=None, **params): - """ Filter a erros by select and ignore lists. + """ Filter a erros by select and ignore options. :return bool: diff --git a/pylibs/pylama/hook.py b/pylibs/pylama/hook.py index 0f3bddad..2a3a5b6c 100644 --- a/pylibs/pylama/hook.py +++ b/pylibs/pylama/hook.py @@ -7,6 +7,7 @@ from subprocess import Popen, PIPE from .main import LOGGER +from .config import parse_options, setup_logger try: @@ -32,8 +33,12 @@ def git_hook(): from .main import check_files _, files_modified, _ = run("git diff-index --cached --name-only HEAD") - LOGGER.setLevel('WARN') - check_files([f for f in map(str, files_modified) if f.endswith('.py')]) + + options = parse_options() + setup_logger(options) + check_files( + [f for f in map(str, files_modified) if f.endswith('.py')], options + ) def hg_hook(ui, repo, node=None, **kwargs): @@ -51,8 +56,10 @@ def hg_hook(ui, repo, node=None, **kwargs): seen.add(file_) if file_.endswith('.py'): paths.append(file_) - LOGGER.setLevel('WARN') - check_files(paths) + + options = parse_options() + setup_logger(options) + check_files(paths, options) def install_git(path): diff --git a/pylibs/pylama/inirama.py b/pylibs/pylama/inirama.py index 8e665724..44a196cb 100644 --- a/pylibs/pylama/inirama.py +++ b/pylibs/pylama/inirama.py @@ -1,16 +1,6 @@ """ Inirama is a python module that parses INI files. - .. _badges: - .. include:: ../README.rst - :start-after: .. _badges: - :end-before: .. _contents: - - .. _description: - .. include:: ../README.rst - :start-after: .. _description: - :end-before: .. _badges: - :copyright: 2013 by Kirill Klenov. :license: BSD, see LICENSE for more details. """ diff --git a/pylibs/pylama/main.py b/pylibs/pylama/main.py index 4b449746..3b0231c3 100644 --- a/pylibs/pylama/main.py +++ b/pylibs/pylama/main.py @@ -1,16 +1,12 @@ -""" Pylama shell integration. +""" Pylama's shell support. """ from __future__ import absolute_import, with_statement -import fnmatch -import logging -import re import sys -from argparse import ArgumentParser -from os import getcwd, walk, path as op +from os import walk, path as op -from . import utils, version -from .core import DEFAULT_LINTERS, LOGGER, STREAM +from .config import parse_options, CURDIR, setup_logger +from .core import LOGGER DEFAULT_COMPLEXITY = 10 @@ -19,119 +15,36 @@ def shell(args=None, error=True): """ Endpoint for console. + Parse a command arguments, configuration files and run a checkers. + :return list: list of errors :raise SystemExit: """ - curdir = getcwd() if args is None: args = sys.argv[1:] - parser = ArgumentParser(description="Code audit tool for python.") - parser.add_argument("path", nargs='?', default=curdir, - help="Path on file or directory.") - parser.add_argument( - "--verbose", "-v", action='store_true', help="Verbose mode.") - parser.add_argument('--version', action='version', - version='%(prog)s ' + version) - - split_csp_list = lambda s: list(set(i for i in s.split(',') if i)) - - parser.add_argument( - "--format", "-f", default='pep8', choices=['pep8', 'pylint'], - help="Error format.") - parser.add_argument( - "--select", "-s", default='', - type=split_csp_list, - help="Select errors and warnings. (comma-separated)") - parser.add_argument( - "--linters", "-l", default=','.join(DEFAULT_LINTERS), - type=split_csp_list, - help=( - "Select linters. (comma-separated). Choices are %s." - % ','.join(s for s in utils.__all__) - )) - parser.add_argument( - "--ignore", "-i", default='', - type=split_csp_list, - help="Ignore errors and warnings. (comma-separated)") - parser.add_argument( - "--skip", default='', - type=lambda s: [re.compile(fnmatch.translate(p)) - for p in s.split(',')], - help="Skip files by masks (comma-separated, Ex. */messages.py)") - parser.add_argument("--complexity", "-c", default=DEFAULT_COMPLEXITY, - type=int, help="Set mccabe complexity.") - parser.add_argument("--report", "-r", help="Filename for report.") - parser.add_argument("--hook", action="store_true", - help="Install Git (Mercurial) hook.") - parser.add_argument( - "--async", action="store_true", - help="Enable async mode. Usefull for checking a lot of files. " - "Dont supported with pylint.") - parser.add_argument( - "--options", "-o", default=op.join(curdir, 'pylama.ini'), - help="Select configuration file. By default is '/pylama.ini'") - options = parser.parse_args(args) - actions = dict((a.dest, a) for a in parser._actions) - - # Read options from configuration file - from .inirama import Namespace - - config = Namespace() - config.default_section = 'main' - config.read(options.options) - for k, v in config.default.items(): - action = actions.get(k) - if action: - LOGGER.info('Find option %s (%s)', k, v) - name, value = action.dest, action.type(v)\ - if callable(action.type) else v - if action.const: - value = bool(int(value)) - setattr(options, name, value) - - # Setup LOGGER - LOGGER.setLevel(logging.INFO if options.verbose else logging.WARN) - if options.report: - LOGGER.removeHandler(STREAM) - LOGGER.addHandler(logging.FileHandler(options.report, mode='w')) - LOGGER.info('Try to read configuration from: ' + options.options) + options = parse_options(args) + setup_logger(options) # Install VSC hook if options.hook: from .hook import install_hook - install_hook(options.path) - - else: - - paths = [options.path] - - if op.isdir(options.path): - paths = [] - for root, _, files in walk(options.path): - paths += [ - op.relpath(op.join(root, f), curdir) - for f in files if f.endswith('.py')] - - return check_files( - paths, - async=options.async, - rootpath=curdir, - skip=options.skip, - frmt=options.format, - ignore=options.ignore, - select=options.select, - linters=options.linters, - complexity=options.complexity, - config=config, - error=error, - ) - - -def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, - select=None, ignore=None, linters=DEFAULT_LINTERS, - complexity=DEFAULT_COMPLEXITY, config=None, error=True): + return install_hook(options.path) + + paths = [options.path] + + if op.isdir(options.path): + paths = [] + for root, _, files in walk(options.path): + paths += [ + op.relpath(op.join(root, f), CURDIR) + for f in files if f.endswith('.py')] + + return check_files(paths, options, error=error) + + +def check_files(paths, options, rootpath=None, error=True): """ Check files. :return list: list of errors @@ -140,28 +53,21 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, """ from .tasks import async_check_files - rootpath = rootpath or getcwd() + if rootpath is None: + rootpath = CURDIR + pattern = "%(rel)s:%(lnum)s:%(col)s: %(text)s" - if frmt == 'pylint': + if options.format == 'pylint': pattern = "%(rel)s:%(lnum)s: [%(type)s] %(text)s" - params = dict() - if config: - for key, section in config.sections.items(): - if key != config.default_section: - mask = re.compile(fnmatch.translate(key)) - params[mask] = prepare_params(section) - work_paths = [] for path in paths: - if skip and any(pattern.match(path) for pattern in skip): + if options.skip and any(p.match(path) for p in options.skip): LOGGER.info('Skip path: %s', path) continue work_paths.append(path) - errors = async_check_files( - work_paths, async=async, rootpath=rootpath, ignore=ignore, - select=select, linters=linters, complexity=complexity, params=params) + errors = async_check_files(work_paths, options, rootpath=rootpath) for er in errors: LOGGER.warning(pattern, er) @@ -172,16 +78,5 @@ def check_files(paths, rootpath=None, skip=None, frmt="pep8", async=False, return errors -def prepare_params(section): - """ Parse modeline params from configuration. - - :return dict: Linter params. - - """ - params = dict(section) - params['lint'] = int(params.get('lint', 1)) - return params - - if __name__ == '__main__': shell() diff --git a/pylibs/pylama/tasks.py b/pylibs/pylama/tasks.py index 485b8905..250b07ee 100644 --- a/pylibs/pylama/tasks.py +++ b/pylibs/pylama/tasks.py @@ -40,7 +40,7 @@ def run(self): self.path_queue.task_done() -def async_check_files(paths, async=False, linters=None, **params): +def async_check_files(paths, options, rootpath=None): """ Check paths. :return list: list of errors @@ -50,12 +50,11 @@ def async_check_files(paths, async=False, linters=None, **params): errors = [] # Disable async if pylint enabled - async = async and not 'pylint' in linters - params['linters'] = linters + async = options.async and not 'pylint' in options.linters if not async: for path in paths: - errors += check_path(path, **params) + errors += check_path(path, options=options, rootpath=rootpath) return errors LOGGER.info('Async code checking is enabled.') @@ -68,7 +67,7 @@ def async_check_files(paths, async=False, linters=None, **params): worker.start() for path in paths: - path_queue.put((path, params)) + path_queue.put((path, dict(options=options, rootpath=rootpath))) path_queue.join() @@ -81,8 +80,7 @@ def async_check_files(paths, async=False, linters=None, **params): return errors -def check_path(path, rootpath='.', ignore=None, select=None, linters=None, - complexity=None, params=None): +def check_path(path, options=None, rootpath=None, **meta): """ Check path. :return list: list of errors @@ -90,17 +88,19 @@ def check_path(path, rootpath='.', ignore=None, select=None, linters=None, """ LOGGER.info("Parse file: %s", path) - params = params or dict() config = dict() + if rootpath is None: + rootpath = '.' - for mask in params: + for mask in options.file_params: if mask.match(path): - config.update(params[mask]) + config.update(options.file_params[mask]) errors = [] for error in run( - path, ignore=ignore, select=select, linters=linters, - complexity=complexity, config=config): + path, ignore=options.ignore, select=options.select, + linters=options.linters, complexity=options.complexity, + config=config, **meta): try: error['rel'] = op.relpath(error['filename'], rootpath) error['col'] = error.get('col', 1) diff --git a/pylibs/pymode/lint.py b/pylibs/pymode/lint.py index 96a7134a..3f19dfa5 100644 --- a/pylibs/pymode/lint.py +++ b/pylibs/pymode/lint.py @@ -1,15 +1,11 @@ """ Pylama support. """ from __future__ import absolute_import -import fnmatch import json import locale -from os import path as op -from re import compile as re -from pylama.core import run -from pylama.inirama import Namespace -from pylama.main import prepare_params +from pylama.main import parse_options +from pylama.tasks import check_path from . import interface from .queue import add_task @@ -23,56 +19,23 @@ def check_file(): """ Check current buffer. """ - checkers = interface.get_option('lint_checker').split(',') buf = interface.get_current_buffer() - # Check configuration from `pymode.ini` - curdir = interface.eval_code('getcwd()') - config = Namespace() - config.default_section = 'main' - config.read(op.join(curdir, 'pylama.ini'), op.join(curdir, 'pymode.ini')) - - ignore = set([ - i for i in ( - interface.get_option('lint_ignore').split(',') + - interface.get_var('lint_ignore').split(',') + - config.default.get('ignore', '').split(',') - ) if i - ]) - - select = set([ - s for s in ( - interface.get_option('lint_select').split(',') + - interface.get_var('lint_select').split(',') + - config.default.get('select', '').split(',') - ) if s - ]) - - complexity = int(interface.get_option('lint_mccabe_complexity') or 0) - - params = dict() - relpath = op.relpath(buf.name, curdir) - for mask in config.sections: - mask_re = re(fnmatch.translate(mask)) - if mask_re.match(relpath): - params.update(prepare_params(config[mask])) - - if relpath in config: - params = prepare_params(config[relpath]) + linters = interface.get_option('lint_checker') + ignore = interface.get_option('lint_ignore') + select = interface.get_option('lint_select') + complexity = interface.get_option('lint_mccabe_complexity') or '0' - add_task( - run_checkers, - callback=parse_result, - title='Code checking', + options = parse_options( + ignore=ignore, select=select, complexity=complexity, linters=linters) - # params - checkers=checkers, ignore=ignore, buf=buf, select=select, - complexity=complexity, config=params, + add_task( + run_checkers, callback=parse_result, title='Code checking', buf=buf, + options=options, ) -def run_checkers(checkers=None, ignore=None, buf=None, select=None, - complexity=None, callback=None, config=None): +def run_checkers(callback=None, buf=None, options=None): """ Run pylama code. :return list: errors @@ -81,9 +44,7 @@ def run_checkers(checkers=None, ignore=None, buf=None, select=None, pylint_options = '--rcfile={0} -r n'.format( interface.get_var('lint_config')).split() - return run( - buf.name, ignore=ignore, select=select, linters=checkers, - pylint=pylint_options, complexity=complexity, config=config) + return check_path(buf.name, options=options, pylint=pylint_options) def parse_result(result, buf=None, **kwargs): From dce446a6419061ddd2b715b3ba851b0c32ce6999 Mon Sep 17 00:00:00 2001 From: Lawrence Jacob Siebert Date: Sun, 4 Aug 2013 18:49:26 -0700 Subject: [PATCH 041/582] Update README.rst better english finded is not a word, and be is not past tense. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf930d06..70b969d6 100644 --- a/README.rst +++ b/README.rst @@ -165,7 +165,7 @@ Default values: :: " Check code every save let g:pymode_lint_write = 1 - " Auto open cwindow if errors be finded + " Auto open cwindow if errors were found let g:pymode_lint_cwindow = 1 " Show error message if cursor placed at the error line From 5f2a0f78f2d9ecb1c0d12a1c7465caed167db0b0 Mon Sep 17 00:00:00 2001 From: Nathan Hoad Date: Wed, 7 Aug 2013 06:39:13 +1000 Subject: [PATCH 042/582] Don't use a t.co link for the screencast link. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index cf930d06..e32bdb62 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ There is no need to install the pylint_, rope_ or any used python library on you - Powerful customization - And more, more ... -See (very old) screencast here: http://t.co/3b0bzeXA (sorry for quality, this is my first screencast) +See (very old) screencast here: http://www.youtube.com/watch?v=67OZNp9Z0CQ (sorry for quality, this is my first screencast) Another old presentation here: http://www.youtube.com/watch?v=YhqsjUUHj6g From 6e2f135c390b4fc0c2e8804c367ee32196f16aa4 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Sat, 17 Aug 2013 23:43:58 +0200 Subject: [PATCH 043/582] Allow Pyrun to deal with sys.exit termination --- autoload/pymode/run.vim | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index df971178..2d07d9b3 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -17,7 +17,18 @@ fun! pymode#run#Run(line1, line2) "{{{ try py context = globals() py context['raw_input'] = context['input'] = lambda s: vim.eval('input("{0}")'.format(s)) - py execfile(vim.eval('expand("%:p")'), context) +python << ENDPYTHON +try: + execfile(vim.eval('expand("%:p")'), context) +# Vim cannot handle a SystemExit error raised by Python, so we need to trap it here, and +# handle it specially +except SystemExit as e: + if e.code: + # A non-false code indicates abnormal termination. A false code will be treated as a + # successful run, and the error will be hidden from Vim + vim.command('echohl Error | echo "Script exited with code {0}" | echohl none'.format(e.code)) + vim.command('return') +ENDPYTHON py out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue() py sys.stdout, sys.stderr = stdout_, stderr_ @@ -38,7 +49,7 @@ else: EOF catch /.*/ - + echohl Error | echo "Run-time error." | echohl none endtry From 1d0969e9e5e1c7b0e0cfb70f1ea84bdddb32dfc3 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Mon, 19 Aug 2013 23:06:22 +0200 Subject: [PATCH 044/582] Fixing the quickfix window output. Better errorformat --- autoload/pymode/run.vim | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index 2d07d9b3..cc72c8a7 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -31,11 +31,23 @@ except SystemExit as e: ENDPYTHON py out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue() py sys.stdout, sys.stderr = stdout_, stderr_ - cexpr "" - py for x in err.strip().split('\n'): vim.command('caddexpr "' + x.replace('"', r'\"') + '"') + let l:traceback = [] let l:oldefm = &efm - set efm=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m + let &efm = '%E File "%f"\, line %l\,%m,' + let &efm .= '%E File "%f"\, line %l,' + let &efm .= '%C%p^,' + let &efm .= '%+C %.%#,' + let &efm .= '%+C %.%#,' + let &efm .= '%Z%m,' + let &efm .= '%-G%.%#' + +python << ENDPYTHON2 +for x in [i for i in err.splitlines() if "" not in i]: + vim.command("call add(l:traceback, '{}')".format(x)) +ENDPYTHON2 + + cgetexpr(l:traceback) call pymode#QuickfixOpen(0, g:pymode_lint_hold, g:pymode_lint_maxheight, g:pymode_lint_minheight, 0) let &efm = l:oldefm @@ -49,8 +61,8 @@ else: EOF catch /.*/ - + echohl Error | echo "Run-time error." | echohl none - + endtry endfunction "}}} From e1e63cc72130d03bdce991eadb9bf84dfe9b3ae9 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Wed, 21 Aug 2013 17:07:04 +0100 Subject: [PATCH 045/582] Adding 'Traceback' line to quickfix errors, for clarity --- autoload/pymode/run.vim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index cc72c8a7..8af5a8d3 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -34,7 +34,8 @@ ENDPYTHON cexpr "" let l:traceback = [] let l:oldefm = &efm - let &efm = '%E File "%f"\, line %l\,%m,' + let &efm = '%+GTraceback%.%#,' + let &efm .= '%E File "%f"\, line %l\,%m,' let &efm .= '%E File "%f"\, line %l,' let &efm .= '%C%p^,' let &efm .= '%+C %.%#,' From a064c3c2983e7a6a07d513628e100b60726563a8 Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Wed, 21 Aug 2013 17:37:44 +0100 Subject: [PATCH 046/582] Commenting the errorformat string --- autoload/pymode/run.vim | 46 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index 8af5a8d3..cbb3b81f 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -34,20 +34,58 @@ ENDPYTHON cexpr "" let l:traceback = [] let l:oldefm = &efm + + " The following lines set Vim's errorformat variable, to allow the + " quickfix window to show Python tracebacks properly. It is much + " easier to use let than set, because set requires many more + " characters to be escaped. This is much easier to read and + " maintain. % escapes are still needed however before any regex meta + " characters. Hence \S (non-whitespace) becomes %\S etc. Note that + " * becomes %#, so .* (match any character) becomes %.%# Commas must + " also be escaped, with a backslash (\,). See the Vim help on + " quickfix for details. + " + " Python errors are multi-lined. They often start with 'Traceback', so + " we want to capture that (with +G) and show it in the quickfix window + " because it explains the order of error messages. let &efm = '%+GTraceback%.%#,' - let &efm .= '%E File "%f"\, line %l\,%m,' - let &efm .= '%E File "%f"\, line %l,' + + " The error message itself starts with a line with 'File' in it. There + " are a couple of variations, and we need to process a line beginning + " with whitespace followed by File, the filename in "", a line number, + " and optional further text. %E here indicates the start of a multi-line + " error message. The %\C at the end means that a case-sensitive search is + " required. + let &efm .= '%E File "%f"\, line %l\,%m%\C,' + let &efm .= '%E File "%f"\, line %l%\C,' + + " The possible continutation lines are idenitifed to Vim by %C. We deal + " with these in order of most to least specific to ensure a proper + " match. A pointer (^) identifies the column in which the error occurs + " (but will not be entirely accurate due to indention of Python code). let &efm .= '%C%p^,' + " Any text, indented by more than two spaces contain useful information. + " We want this to appear in the quickfix window, hence %+. let &efm .= '%+C %.%#,' let &efm .= '%+C %.%#,' - let &efm .= '%Z%m,' + + " The last line (%Z) does not begin with any whitespace. We use a zero + " width lookahead (\&) to check this. The line contains the error + " message itself (%m) + let &efm .= '%Z%\S%\&%m,' + + " We can ignore any other lines (%-G) let &efm .= '%-G%.%#' python << ENDPYTHON2 +# Remove any error lines containing ''. We don't need them. +# Add the rest to a Vim list. for x in [i for i in err.splitlines() if "" not in i]: vim.command("call add(l:traceback, '{}')".format(x)) ENDPYTHON2 - +" Now we can add the list of errors to the quickfix window, and show it. We have +" to add them all at once in this way, because the errors are multi-lined and +" they won't be parsed properly otherwise. cgetexpr(l:traceback) call pymode#QuickfixOpen(0, g:pymode_lint_hold, g:pymode_lint_maxheight, g:pymode_lint_minheight, 0) let &efm = l:oldefm From 57aaea51bfbc5ecdb4297fb14bc546dd415efbdc Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sat, 24 Aug 2013 15:29:54 +0530 Subject: [PATCH 047/582] Add pymode#Execute function to wrap calls to python interpreter. Extract setting up sys.path responsibility into a separate autoload module. Modify run.vim to support pymode#Execute. --- autoload/pymode.vim | 7 +++++++ autoload/pymode/path.vim | 12 ++++++++++++ autoload/pymode/run.vim | 23 +++++++++++++---------- ftplugin/python/init-pymode.vim | 20 ++++++-------------- ftplugin/python/pymode.vim | 2 +- 5 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 autoload/pymode/path.vim diff --git a/autoload/pymode.vim b/autoload/pymode.vim index c69e6798..89f19a21 100644 --- a/autoload/pymode.vim +++ b/autoload/pymode.vim @@ -12,6 +12,13 @@ fun! pymode#Default(name, default) "{{{ endfunction "}}} +fun! pymode#Execute(expression) "{{{ + " DESC: Execute an expression in the default python interpreter + " + execute 'python '.a:expression +endfunction "}}} + + fun! pymode#Option(name) "{{{ let name = 'b:pymode_' . a:name diff --git a/autoload/pymode/path.vim b/autoload/pymode/path.vim new file mode 100644 index 00000000..b692dbe1 --- /dev/null +++ b/autoload/pymode/path.vim @@ -0,0 +1,12 @@ +fun! pymode#path#Activate(plugin_root) "{{{ + +python << EOF +import sys, vim, os + +curpath = vim.eval("getcwd()") +libpath = os.path.join(vim.eval("a:plugin_root"), 'pylibs') + +sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path +EOF + +endfunction "}}} diff --git a/autoload/pymode/run.vim b/autoload/pymode/run.vim index df971178..5933f372 100644 --- a/autoload/pymode/run.vim +++ b/autoload/pymode/run.vim @@ -8,21 +8,24 @@ fun! pymode#run#Run(line1, line2) "{{{ return 0 endtry endif - py import StringIO - py sys.stdout, stdout_ = StringIO.StringIO(), sys.stdout - py sys.stderr, stderr_ = StringIO.StringIO(), sys.stderr - py enc = vim.eval('&enc') + call pymode#Execute("import StringIO") + call pymode#Execute("sys.stdout, stdout_ = StringIO.StringIO(), sys.stdout") + call pymode#Execute("sys.stderr, stderr_ = StringIO.StringIO(), sys.stderr") + call pymode#Execute("enc = vim.eval('&enc')") call setqflist([]) call pymode#WideMessage("Code running.") try - py context = globals() - py context['raw_input'] = context['input'] = lambda s: vim.eval('input("{0}")'.format(s)) - py execfile(vim.eval('expand("%:p")'), context) - py out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue() - py sys.stdout, sys.stderr = stdout_, stderr_ + call pymode#Execute("context = globals()") + call pymode#Execute("context['raw_input'] = context['input'] = lambda s: vim.eval('input(\"{0}\")'.format(s))") + call pymode#Execute("execfile(vim.eval('expand(\"%:p\")'), context)") + call pymode#Execute("out, err = sys.stdout.getvalue().strip(), sys.stderr.getvalue()") + call pymode#Execute("sys.stdout, sys.stderr = stdout_, stderr_") cexpr "" - py for x in err.strip().split('\n'): vim.command('caddexpr "' + x.replace('"', r'\"') + '"') +python << EOF +for x in err.strip().split('\n'): + vim.command('caddexpr "' + x.replace('"', r'\"') + '"') +EOF let l:oldefm = &efm set efm=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m call pymode#QuickfixOpen(0, g:pymode_lint_hold, g:pymode_lint_maxheight, g:pymode_lint_minheight, 0) diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 96ecd74a..506028cb 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -42,15 +42,7 @@ endif if !pymode#Default('g:pymode_path', 1) || g:pymode_path call pymode#Default('g:pymode_paths', []) - -python << EOF -import sys, vim, os - -curpath = vim.eval("getcwd()") -libpath = os.path.join(vim.eval("expand(':p:h:h:h')"), 'pylibs') - -sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path -EOF + call pymode#path#Activate(expand(":p:h:h:h")) endif " }}} @@ -130,9 +122,9 @@ if !pymode#Default("g:pymode_lint", 1) || g:pymode_lint let g:pymode_lint_config = expand(":p:h:h:h") . "/pylint.ini" endif - py from pymode import queue + call pymode#Execute("from pymode import queue") - au VimLeavePre * py queue.stop_queue() + au VimLeavePre * call pymode#Execute("queue.stop_queue()") endif @@ -252,7 +244,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope call pymode#Default("g:pymode_rope_always_show_complete_menu", 0) " DESC: Init Rope - py import ropevim + call pymode#Execute("import ropevim") fun! RopeCodeAssistInsertMode() "{{{ call RopeCodeAssist() @@ -263,7 +255,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope if isdirectory(getcwd() . '/.ropeproject') " In order to pass it the quiet kwarg I need to open the project " using python and not vim, which should be no major issue - py ropevim._interface.open_project(quiet=True) + call pymode#Execute("ropevim._interface.open_project(quiet=True)") return "" endif endfunction "}}} @@ -275,7 +267,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope fun! RopeOmni(findstart, base) "{{{ if a:findstart - py ropevim._interface._find_start() + call pymode#Execute("ropevim._interface._find_start()") return g:pymode_offset else call RopeOmniComplete() diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 9b85421e..1560e2e9 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -76,7 +76,7 @@ if pymode#Option('lint') " DESC: Run queue let &l:updatetime = g:pymode_updatetime au CursorHold call pymode#queue#Poll() - au BufLeave py queue.stop_queue() + au BufLeave call pymode#Execute("queue.stop_queue()") endif From 225508defe29a55b24892641c1cf2717c1afdf68 Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sat, 24 Aug 2013 15:58:16 +0530 Subject: [PATCH 048/582] Add pymode#Execute to doc, lint and queue. --- autoload/pymode/doc.vim | 10 +++++----- autoload/pymode/lint.vim | 8 ++++---- autoload/pymode/queue.vim | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/autoload/pymode/doc.vim b/autoload/pymode/doc.vim index cf997ad8..27e5b5eb 100644 --- a/autoload/pymode/doc.vim +++ b/autoload/pymode/doc.vim @@ -5,12 +5,12 @@ fun! pymode#doc#Show(word) "{{{ if a:word == '' echoerr "No name/symbol under cursor!" else - py import StringIO - py sys.stdout, _ = StringIO.StringIO(), sys.stdout - py help(vim.eval('a:word')) - py sys.stdout, out = _, sys.stdout.getvalue() + call pymode#Execute("import StringIO") + call pymode#Execute("sys.stdout, _ = StringIO.StringIO(), sys.stdout") + call pymode#Execute("help('".a:word."')") + call pymode#Execute("sys.stdout, out = _, sys.stdout.getvalue()") call pymode#TempBuffer() - py vim.current.buffer.append(str(out).split('\n'), 0) + call pymode#Execute("vim.current.buffer.append(str(out).splitlines(), 0)") wincmd p endif endfunction "}}} diff --git a/autoload/pymode/lint.vim b/autoload/pymode/lint.vim index 68782e18..37e9d4fa 100644 --- a/autoload/pymode/lint.vim +++ b/autoload/pymode/lint.vim @@ -14,8 +14,8 @@ fun! pymode#lint#Check() "{{{ let g:pymode_lint_buffer = bufnr('%') - py from pymode import lint - py lint.check_file() + call pymode#Execute("from pymode import lint") + call pymode#Execute("lint.check_file()") endfunction " }}} @@ -100,8 +100,8 @@ fun! pymode#lint#Auto() "{{{ return 0 endtry endif - py from pymode import auto - py auto.fix_current_file() + call pymode#Execute("from pymode import auto") + call pymode#Execute("auto.fix_current_file()") cclose edit call pymode#WideMessage("AutoPep8 done.") diff --git a/autoload/pymode/queue.vim b/autoload/pymode/queue.vim index b3160ee0..656049a7 100644 --- a/autoload/pymode/queue.vim +++ b/autoload/pymode/queue.vim @@ -1,7 +1,8 @@ fun! pymode#queue#Poll() "{{{ " Check current tasks - py queue.check_task() + call pymode#Execute("from pymode import queue") + call pymode#Execute("queue.check_task()") " Update interval if mode() == 'i' From b56321512a76b2d273bf04a7c7ef1cc9fdb5bd09 Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sun, 25 Aug 2013 09:54:21 +0530 Subject: [PATCH 049/582] Do not indent and other ftplugin if g:pymode is zero. --- after/indent/python.vim | 4 ++++ ftplugin/python/pymode.vim | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/after/indent/python.vim b/after/indent/python.vim index 1e324f23..969488c3 100644 --- a/after/indent/python.vim +++ b/after/indent/python.vim @@ -1,3 +1,7 @@ +if pymode#Default('g:pymode', 1) || !g:pymode + finish +endif + if pymode#Default('b:pymode_indent', 1) || !g:pymode_indent finish endif diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 9b85421e..c4722219 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -1,5 +1,9 @@ runtime ftplugin/python/init-pymode.vim +if pymode#Default('g:pymode', 1) || !g:pymode + finish +endif + if pymode#Default('b:pymode', 1) finish endif From 6b94dc20761bd9430dd52e2818b810af213b5530 Mon Sep 17 00:00:00 2001 From: Arun Mahapatra Date: Sun, 25 Aug 2013 10:00:11 +0530 Subject: [PATCH 050/582] Remove unnecessary check for pymode state in buffer scope. --- ftplugin/python/pymode.vim | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index c4722219..de9bc680 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -4,10 +4,6 @@ if pymode#Default('g:pymode', 1) || !g:pymode finish endif -if pymode#Default('b:pymode', 1) - finish -endif - " Parse pymode modeline call pymode#Modeline() From 7f9e3ad7098312a964de85d5e946f651c75eb20c Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 18:46:18 +0700 Subject: [PATCH 051/582] Update changelog. --- Changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.rst b/Changelog.rst index 5536794b..8a166c93 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -5,6 +5,7 @@ Changelog * Removed `g:pymode_rope_map_space` option; * Added PEP257 checker; * Support 'pudb' in breakpoints; +* Pyrun can now operate on a range of lines, and does not need to save (c) lawrenceakka ## 2013-05-15 0.6.18 -------------------- From aa9d049b4492f793217b4ab107b4bf7ad29c3edc Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 18:46:36 +0700 Subject: [PATCH 052/582] Prepare to python3 support --- AUTHORS | 1 + autoload/pymode.vim | 7 ------- autoload/pymode/breakpoint.vim | 3 ++- autoload/pymode/doc.vim | 9 ++++----- autoload/pymode/lint.vim | 8 ++++---- autoload/pymode/path.vim | 29 +++++++++++++++++------------ autoload/pymode/queue.vim | 4 ++-- autoload/pymode/virtualenv.vim | 2 +- ftplugin/python/init-pymode.vim | 19 ++++++++++++------- ftplugin/python/pymode.vim | 4 ++-- 10 files changed, 45 insertions(+), 41 deletions(-) diff --git a/AUTHORS b/AUTHORS index e0bc5bbb..797fb34e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Contributors: * Denis Kasak (dkasak); * Steve Losh (sjl); * nixon; +* lawrenceakka; * tramchamploo; * Benjamin Ruston (bruston); * Robert David Grant (bgrant); diff --git a/autoload/pymode.vim b/autoload/pymode.vim index 89f19a21..c69e6798 100644 --- a/autoload/pymode.vim +++ b/autoload/pymode.vim @@ -12,13 +12,6 @@ fun! pymode#Default(name, default) "{{{ endfunction "}}} -fun! pymode#Execute(expression) "{{{ - " DESC: Execute an expression in the default python interpreter - " - execute 'python '.a:expression -endfunction "}}} - - fun! pymode#Option(name) "{{{ let name = 'b:pymode_' . a:name diff --git a/autoload/pymode/breakpoint.vim b/autoload/pymode/breakpoint.vim index b71bc596..82dd8e82 100644 --- a/autoload/pymode/breakpoint.vim +++ b/autoload/pymode/breakpoint.vim @@ -1,3 +1,4 @@ +" Quick set or delete a breakpoints fun! pymode#breakpoint#Set(lnum) "{{{ let line = getline(a:lnum) if strridx(line, g:pymode_breakpoint_cmd) != -1 @@ -8,7 +9,7 @@ fun! pymode#breakpoint#Set(lnum) "{{{ normal k endif - " Save file + " Save file without any events if &modifiable && &modified | noautocmd write | endif endfunction "}}} diff --git a/autoload/pymode/doc.vim b/autoload/pymode/doc.vim index 27e5b5eb..f5800f99 100644 --- a/autoload/pymode/doc.vim +++ b/autoload/pymode/doc.vim @@ -5,12 +5,11 @@ fun! pymode#doc#Show(word) "{{{ if a:word == '' echoerr "No name/symbol under cursor!" else - call pymode#Execute("import StringIO") - call pymode#Execute("sys.stdout, _ = StringIO.StringIO(), sys.stdout") - call pymode#Execute("help('".a:word."')") - call pymode#Execute("sys.stdout, out = _, sys.stdout.getvalue()") + Python import StringIO + Python sys.stdout, _ = StringIO.StringIO(), sys.stdout + Python help(vim.eval('a:word')) call pymode#TempBuffer() - call pymode#Execute("vim.current.buffer.append(str(out).splitlines(), 0)") + Python vim.current.buffer.append(str(out).splitlines(), 0) wincmd p endif endfunction "}}} diff --git a/autoload/pymode/lint.vim b/autoload/pymode/lint.vim index 37e9d4fa..7e6bc9b2 100644 --- a/autoload/pymode/lint.vim +++ b/autoload/pymode/lint.vim @@ -14,8 +14,8 @@ fun! pymode#lint#Check() "{{{ let g:pymode_lint_buffer = bufnr('%') - call pymode#Execute("from pymode import lint") - call pymode#Execute("lint.check_file()") + Python from pymode import lint + Python lint.check_file() endfunction " }}} @@ -100,8 +100,8 @@ fun! pymode#lint#Auto() "{{{ return 0 endtry endif - call pymode#Execute("from pymode import auto") - call pymode#Execute("auto.fix_current_file()") + Python from pymode import auto + Python auto.fix_current_file() cclose edit call pymode#WideMessage("AutoPep8 done.") diff --git a/autoload/pymode/path.vim b/autoload/pymode/path.vim index b692dbe1..e765ac7d 100644 --- a/autoload/pymode/path.vim +++ b/autoload/pymode/path.vim @@ -1,12 +1,17 @@ -fun! pymode#path#Activate(plugin_root) "{{{ - -python << EOF -import sys, vim, os - -curpath = vim.eval("getcwd()") -libpath = os.path.join(vim.eval("a:plugin_root"), 'pylibs') - -sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path -EOF - -endfunction "}}} +fun! pymode#path#Activate(plugin_root) "{{{ + +Python << EOF +import sys, vim, os + +pymode_lib = 'pylibs' + +if sys.version >= (3, 0, 0): + pymode_lib = 'pylibs3' + +curpath = vim.eval("getcwd()") +libpath = os.path.join(vim.eval("a:plugin_root"), pymode_lib) + +sys.path = [libpath, curpath] + vim.eval("g:pymode_paths") + sys.path +EOF + +endfunction "}}} diff --git a/autoload/pymode/queue.vim b/autoload/pymode/queue.vim index 656049a7..d38208b7 100644 --- a/autoload/pymode/queue.vim +++ b/autoload/pymode/queue.vim @@ -1,8 +1,8 @@ fun! pymode#queue#Poll() "{{{ " Check current tasks - call pymode#Execute("from pymode import queue") - call pymode#Execute("queue.check_task()") + Python from pymode import queue + Python queue.check_task() " Update interval if mode() == 'i' diff --git a/autoload/pymode/virtualenv.vim b/autoload/pymode/virtualenv.vim index c771d907..04267343 100644 --- a/autoload/pymode/virtualenv.vim +++ b/autoload/pymode/virtualenv.vim @@ -12,7 +12,7 @@ fun! pymode#virtualenv#Activate() "{{{ call add(g:pymode_virtualenv_enabled, $VIRTUAL_ENV) -python << EOF +Python << EOF import sys, vim, os ve_dir = vim.eval('$VIRTUAL_ENV') diff --git a/ftplugin/python/init-pymode.vim b/ftplugin/python/init-pymode.vim index 506028cb..a14082e2 100644 --- a/ftplugin/python/init-pymode.vim +++ b/ftplugin/python/init-pymode.vim @@ -13,7 +13,7 @@ if pymode#Default('g:pymode', 1) || !g:pymode endif " DESC: Check python support -if !has('python') +if !has('python') && !has('python3') let g:pymode_virtualenv = 0 let g:pymode_path = 0 let g:pymode_lint = 0 @@ -23,6 +23,11 @@ if !has('python') let g:pymode_run = 0 endif +if has('python') + command! -nargs=1 Python python +elseif has('python3') + command! -nargs=1 Python python3 +end " Virtualenv {{{ @@ -122,9 +127,9 @@ if !pymode#Default("g:pymode_lint", 1) || g:pymode_lint let g:pymode_lint_config = expand(":p:h:h:h") . "/pylint.ini" endif - call pymode#Execute("from pymode import queue") + Python from pymode import queue - au VimLeavePre * call pymode#Execute("queue.stop_queue()") + au VimLeavePre * Python queue.stop_queue() endif @@ -149,7 +154,7 @@ if !pymode#Default("g:pymode_breakpoint", 1) || g:pymode_breakpoint if !pymode#Default("g:pymode_breakpoint_cmd", "import pdb; pdb.set_trace() # XXX BREAKPOINT") && has("python") -python << EOF +Python << EOF from imp import find_module for module in ('pudb', 'ipdb'): @@ -244,7 +249,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope call pymode#Default("g:pymode_rope_always_show_complete_menu", 0) " DESC: Init Rope - call pymode#Execute("import ropevim") + Python import ropevim fun! RopeCodeAssistInsertMode() "{{{ call RopeCodeAssist() @@ -255,7 +260,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope if isdirectory(getcwd() . '/.ropeproject') " In order to pass it the quiet kwarg I need to open the project " using python and not vim, which should be no major issue - call pymode#Execute("ropevim._interface.open_project(quiet=True)") + Python ropevim._interface.open_project(quiet=True) return "" endif endfunction "}}} @@ -267,7 +272,7 @@ if !pymode#Default("g:pymode_rope", 1) || g:pymode_rope fun! RopeOmni(findstart, base) "{{{ if a:findstart - call pymode#Execute("ropevim._interface._find_start()") + Python ropevim._interface._find_start() return g:pymode_offset else call RopeOmniComplete() diff --git a/ftplugin/python/pymode.vim b/ftplugin/python/pymode.vim index 25a839b3..d9b7a755 100644 --- a/ftplugin/python/pymode.vim +++ b/ftplugin/python/pymode.vim @@ -1,6 +1,6 @@ runtime ftplugin/python/init-pymode.vim -if pymode#Default('g:pymode', 1) || !g:pymode +if !g:pymode finish endif @@ -76,7 +76,7 @@ if pymode#Option('lint') " DESC: Run queue let &l:updatetime = g:pymode_updatetime au CursorHold call pymode#queue#Poll() - au BufLeave call pymode#Execute("queue.stop_queue()") + au BufLeave Python queue.stop_queue() endif From f32a5124d03983754ff7f7c46b36460890ab395a Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 19:00:47 +0700 Subject: [PATCH 053/582] Change paths. --- autoload/pymode/path.vim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autoload/pymode/path.vim b/autoload/pymode/path.vim index e765ac7d..a2793e4e 100644 --- a/autoload/pymode/path.vim +++ b/autoload/pymode/path.vim @@ -5,8 +5,8 @@ import sys, vim, os pymode_lib = 'pylibs' -if sys.version >= (3, 0, 0): - pymode_lib = 'pylibs3' +# if sys.version >= (3, 0, 0): +# pymode_lib = 'pylibs3' curpath = vim.eval("getcwd()") libpath = os.path.join(vim.eval("a:plugin_root"), pymode_lib) From b1beaae3fc63c117ef533cd80e3092aba16c231e Mon Sep 17 00:00:00 2001 From: Kirill Klenov Date: Thu, 29 Aug 2013 23:34:03 +0700 Subject: [PATCH 054/582] Update pylama to version 1.5.0 --- Changelog.rst | 1 + pylibs/pylama/__init__.py | 10 +- pylibs/pylama/checkers/mccabe.py | 30 +- pylibs/pylama/checkers/pep257.py | 43 ++- pylibs/pylama/checkers/pep8.py | 5 +- pylibs/pylama/checkers/pylint/__init__.py | 5 +- pylibs/pylama/checkers/pylint/__pkginfo__.py | 5 +- .../checkers/pylint/astroid/__init__.py | 118 ++++++ .../{logilab/astng => astroid}/__pkginfo__.py | 22 +- .../{logilab/astng => astroid}/as_string.py | 142 +++---- .../{logilab/astng => astroid}/bases.py | 51 ++- .../{logilab/astng => astroid}/builder.py | 54 ++- .../{logilab/astng => astroid}/exceptions.py | 29 +- .../{logilab/astng => astroid}/inference.py | 70 ++-- .../{logilab/astng => astroid}/manager.py | 153 ++++---- .../{logilab/astng => astroid}/mixins.py | 12 +- .../astng => astroid}/node_classes.py | 129 ++++--- .../{logilab/astng => astroid}/nodes.py | 10 +- .../{logilab/astng => astroid}/protocols.py | 17 +- .../astng => astroid}/raw_building.py | 60 +-- .../{logilab/astng => astroid}/rebuilder.py | 125 ++++-- .../astng => astroid}/scoped_nodes.py | 107 ++--- .../{logilab/astng => astroid}/utils.py | 30 +- .../checkers/pylint/checkers/__init__.py | 57 ++- .../pylama/checkers/pylint/checkers/base.py | 365 +++++++++++------- .../checkers/pylint/checkers/classes.py | 72 ++-- .../pylint/checkers/design_analysis.py | 94 ++--- .../checkers/pylint/checkers/exceptions.py | 57 +-- .../pylama/checkers/pylint/checkers/format.py | 72 ++-- .../checkers/pylint/checkers/imports.py | 29 +- .../checkers/pylint/checkers/logging.py | 29 +- .../pylama/checkers/pylint/checkers/misc.py | 58 +-- .../checkers/pylint/checkers/newstyle.py | 55 ++- .../checkers/pylint/checkers/raw_metrics.py | 14 +- .../checkers/pylint/checkers/similar.py | 34 +- .../pylama/checkers/pylint/checkers/stdlib.py | 68 ++++ .../checkers/pylint/checkers/strings.py | 41 +- .../checkers/pylint/checkers/typecheck.py | 84 ++-- .../pylama/checkers/pylint/checkers/utils.py | 111 ++++-- .../checkers/pylint/checkers/variables.py | 101 +++-- pylibs/pylama/checkers/pylint/config.py | 16 +- pylibs/pylama/checkers/pylint/interfaces.py | 52 +-- pylibs/pylama/checkers/pylint/lint.py | 197 +++++----- .../checkers/pylint/logilab/astng/__init__.py | 72 ---- .../pylint/logilab/astng/brain/__init__.py | 0 .../logilab/astng/brain/py2mechanize.py | 20 - .../pylint/logilab/astng/brain/py2qt4.py | 25 -- .../pylint/logilab/astng/brain/py2stdlib.py | 182 --------- .../pylint/logilab/common/__pkginfo__.py | 9 +- .../pylint/logilab/common/changelog.py | 2 +- .../pylint/logilab/common/configuration.py | 15 +- .../pylint/logilab/common/deprecation.py | 234 +++++------ .../pylint/logilab/common/modutils.py | 33 +- .../pylint/logilab/common/optik_ext.py | 4 +- .../logilab/common/ureports/docbook_writer.py | 2 +- .../logilab/common/ureports/html_writer.py | 2 +- .../logilab/common/ureports/text_writer.py | 2 +- .../checkers/pylint/reporters/__init__.py | 62 +-- .../checkers/pylint/reporters/guireporter.py | 12 +- .../pylama/checkers/pylint/reporters/html.py | 17 +- .../pylama/checkers/pylint/reporters/text.py | 100 +++-- pylibs/pylama/checkers/pylint/utils.py | 151 +++++--- pylibs/pylama/core.py | 50 ++- pylibs/pylama/hook.py | 5 +- pylibs/pylama/inirama.py | 55 ++- pylibs/pylama/main.py | 3 +- pylibs/pylama/tasks.py | 3 +- pylibs/pylama/utils.py | 7 +- 68 files changed, 2120 insertions(+), 1781 deletions(-) create mode 100644 pylibs/pylama/checkers/pylint/astroid/__init__.py rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/__pkginfo__.py (71%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/as_string.py (78%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/bases.py (92%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/builder.py (83%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/exceptions.py (55%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/inference.py (88%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/manager.py (63%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/mixins.py (91%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/node_classes.py (89%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/nodes.py (88%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/protocols.py (96%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/raw_building.py (87%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/rebuilder.py (90%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/scoped_nodes.py (91%) rename pylibs/pylama/checkers/pylint/{logilab/astng => astroid}/utils.py (90%) create mode 100644 pylibs/pylama/checkers/pylint/checkers/stdlib.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/__init__.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/__init__.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/py2mechanize.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/py2qt4.py delete mode 100644 pylibs/pylama/checkers/pylint/logilab/astng/brain/py2stdlib.py diff --git a/Changelog.rst b/Changelog.rst index 8a166c93..bf059336 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -6,6 +6,7 @@ Changelog * Added PEP257 checker; * Support 'pudb' in breakpoints; * Pyrun can now operate on a range of lines, and does not need to save (c) lawrenceakka +* Update pylama to version 1.5.0 ## 2013-05-15 0.6.18 -------------------- diff --git a/pylibs/pylama/__init__.py b/pylibs/pylama/__init__.py index 3d7ddc52..c3cb1d7b 100644 --- a/pylibs/pylama/__init__.py +++ b/pylibs/pylama/__init__.py @@ -1,11 +1,11 @@ -""" - Code audit tool for python. +""" Code audit tool for python. + +:copyright: 2013 by Kirill Klenov. +:license: BSD, see LICENSE for more details. - :copyright: 2013 by Kirill Klenov. - :license: BSD, see LICENSE for more details. """ -version_info = 1, 3, 3 +version_info = 1, 5, 0 __version__ = version = '.'.join(map(str, version_info)) __project__ = __name__ diff --git a/pylibs/pylama/checkers/mccabe.py b/pylibs/pylama/checkers/mccabe.py index 71f024a9..4ea34be0 100644 --- a/pylibs/pylama/checkers/mccabe.py +++ b/pylibs/pylama/checkers/mccabe.py @@ -3,17 +3,18 @@ http://nedbatchelder.com/blog/200803/python_code_complexity_microtool.html MIT License. """ -from __future__ import absolute_import, with_statement +from __future__ import with_statement -import sys - -import ast import optparse -from ast import iter_child_nodes +import sys from collections import defaultdict +try: + import ast + from ast import iter_child_nodes +except ImportError: # Python 2.5 + from flake8.util import ast, iter_child_nodes - -__version__ = '0.2' +__version__ = '0.2.1' class ASTVisitor(object): @@ -263,14 +264,13 @@ def get_code_complexity(code, threshold=7, filename='stdin'): complx = [] McCabeChecker.max_complexity = threshold - for lineno, offset, text, _ in McCabeChecker(tree, filename).run(): - complx.append(dict( - type=McCabeChecker._code, - lnum=lineno, - text=text, - )) + for lineno, offset, text, check in McCabeChecker(tree, filename).run(): + complx.append('%s:%d:1: %s' % (filename, lineno, text)) - return complx + if len(complx) == 0: + return 0 + print('\n'.join(complx)) + return len(complx) def get_module_complexity(module_path, threshold=7): @@ -310,5 +310,3 @@ def main(argv): if __name__ == '__main__': main(sys.argv[1:]) - -# lint=0 diff --git a/pylibs/pylama/checkers/pep257.py b/pylibs/pylama/checkers/pep257.py index aab6a91f..a43dbae7 100644 --- a/pylibs/pylama/checkers/pep257.py +++ b/pylibs/pylama/checkers/pep257.py @@ -54,6 +54,7 @@ def your_check(class_docstring, context, is_script): Also, see examples in "Check functions" section. """ +__version__ = '0.2.4' from curses.ascii import isascii import inspect @@ -150,6 +151,23 @@ def rel_pos(abs_pos, source): return len(lines) + 1, abs_pos - len(''.join(lines)) +def get_summary_line_info(thedocstring): + """Get the (summary_line, line_number) tuple for the given docstring. + + The returned 'summary_line' is the pep257 summary line and 'line_number' is + the zero-based docstring line number containing the summary line, which + will be either 0 (zeroth line) or 1 (first line). Any docstring checks + relating to the summary line should use this method to ensure consistent + treatment of the summary line. + + """ + lines = eval(thedocstring).split('\n') + first_line = lines[0].strip() + if len(lines) == 1 or len(first_line) > 0: + return first_line, 0 + return lines[1].strip(), 1 + + # # Parsing # @@ -167,8 +185,11 @@ def parse_module_docstring(source): def parse_docstring(source, what=''): """Parse docstring given `def` or `class` source.""" + module_docstring = parse_module_docstring(source) if what.startswith('module'): - return parse_module_docstring(source) + return module_docstring + if module_docstring: + return module_docstring token_gen = tk.generate_tokens(StringIO(source).readline) try: kind = None @@ -249,7 +270,7 @@ def parse_contexts(source, kind): if kind == 'def_docstring': return parse_functions(source) + parse_methods(source) if kind == 'docstring': - return ([source] + parse_functions(source) + + return ([parse_module_docstring(source)] + parse_functions(source) + parse_classes(source) + parse_methods(source)) @@ -378,7 +399,7 @@ def check_files(filenames): def parse_options(): - parser = OptionParser() + parser = OptionParser(version=__version__) parser.add_option('-e', '--explain', action='store_true', help='show explanation of each error') parser.add_option('-r', '--range', action='store_true', @@ -418,6 +439,7 @@ def main(options, arguments): f.close() for error in sorted(errors): print_error(str(error)) + return 1 if errors else 0 # @@ -546,7 +568,10 @@ def check_ends_with_period(docstring, context, is_script): The [first line of a] docstring is a phrase ending in a period. """ - if docstring and not eval(docstring).split('\n')[0].strip().endswith('.'): + if not docstring: + return + (summary_line, line_number) = get_summary_line_info(docstring) + if not summary_line.endswith('.'): return True @@ -610,8 +635,10 @@ def check_blank_after_summary(docstring, context, is_script): if not docstring: return lines = eval(docstring).split('\n') - if len(lines) > 1 and lines[1].strip() != '': - return True + if len(lines) > 1: + (summary_line, line_number) = get_summary_line_info(docstring) + if len(lines) <= (line_number+1) or lines[line_number+1].strip() != '': + return True def check_indent(docstring, context, is_script): @@ -645,7 +672,7 @@ def check_blank_before_after_class(class_docstring, context, is_script): """ if not class_docstring: return - before, after = context.split(class_docstring) + before, after = context.split(class_docstring)[:2] before_blanks = [not line.strip() for line in before.split('\n')] after_blanks = [not line.strip() for line in after.split('\n')] if before_blanks[-3:] != [False, True, True]: @@ -671,6 +698,6 @@ def check_blank_after_last_paragraph(docstring, context, is_script): if __name__ == '__main__': try: - main(*parse_options()) + sys.exit(main(*parse_options())) except KeyboardInterrupt: pass diff --git a/pylibs/pylama/checkers/pep8.py b/pylibs/pylama/checkers/pep8.py index 8413270f..e0035b3d 100644 --- a/pylibs/pylama/checkers/pep8.py +++ b/pylibs/pylama/checkers/pep8.py @@ -45,7 +45,7 @@ 700 statements 900 syntax error """ -__version__ = '1.4.6' +__version__ = '1.4.7a0' import os import sys @@ -1678,6 +1678,9 @@ def ignore_code(self, code): return False. Else, if 'options.ignore' contains a prefix of the error code, return True. """ + if len(code) < 4 and any(s.startswith(code) + for s in self.options.select): + return False return (code.startswith(self.options.ignore) and not code.startswith(self.options.select)) diff --git a/pylibs/pylama/checkers/pylint/__init__.py b/pylibs/pylama/checkers/pylint/__init__.py index 697c8c05..dfb4386b 100644 --- a/pylibs/pylama/checkers/pylint/__init__.py +++ b/pylibs/pylama/checkers/pylint/__init__.py @@ -17,7 +17,7 @@ def run_pylint(): """run pylint""" - from .lint import Run + from pylint.lint import Run Run(sys.argv[1:]) def run_pylint_gui(): @@ -40,6 +40,5 @@ def run_pyreverse(): def run_symilar(): """run symilar""" - from .checkers.similar import Run + from pylint.checkers.similar import Run Run(sys.argv[1:]) - diff --git a/pylibs/pylama/checkers/pylint/__pkginfo__.py b/pylibs/pylama/checkers/pylint/__pkginfo__.py index a018f934..997b9a59 100644 --- a/pylibs/pylama/checkers/pylint/__pkginfo__.py +++ b/pylibs/pylama/checkers/pylint/__pkginfo__.py @@ -18,10 +18,10 @@ modname = distname = 'pylint' -numversion = (0, 28, 0) +numversion = (1, 0, 0) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common >= 0.53.0', 'logilab-astng >= 0.24.3'] +install_requires = ['logilab-common >= 0.53.0', 'astroid >= 0.24.3'] license = 'GPL' description = "python code static checker" @@ -66,3 +66,4 @@ for filename in ('pylint', 'pylint-gui', "symilar", "epylint", "pyreverse")] +include_dirs = ['test'] diff --git a/pylibs/pylama/checkers/pylint/astroid/__init__.py b/pylibs/pylama/checkers/pylint/astroid/__init__.py new file mode 100644 index 00000000..af17875d --- /dev/null +++ b/pylibs/pylama/checkers/pylint/astroid/__init__.py @@ -0,0 +1,118 @@ +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see . +"""Python Abstract Syntax Tree New Generation + +The aim of this module is to provide a common base representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentially +governed by pylint's needs. + +It extends class defined in the python's _ast module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astroid ;) by visiting an existent ast tree or by inspecting living +object. Methods are added by monkey patching ast classes. + +Main modules are: + +* nodes and scoped_nodes for more information about methods and + attributes added to different node classes + +* the manager contains a high level object to get astroid trees from + source files and living objects. It maintains a cache of previously + constructed tree for quick access + +* builder contains the class responsible to build astroid trees +""" +__doctype__ = "restructuredtext en" + +import sys +import re +from operator import attrgetter + +# WARNING: internal imports order matters ! + +# make all exception classes accessible from astroid package +from .exceptions import * + +# make all node classes accessible from astroid package +from .nodes import * + +# trigger extra monkey-patching +from . import inference + +# more stuff available +from . import raw_building +from .bases import YES, Instance, BoundMethod, UnboundMethod +from .node_classes import are_exclusive, unpack_infer +from .scoped_nodes import builtin_lookup + +# make a manager instance (borg) as well as Project and Package classes +# accessible from astroid package +from .manager import AstroidManager, Project +MANAGER = AstroidManager() +del AstroidManager + +# transform utilities (filters and decorator) + +class AsStringRegexpPredicate(object): + """Class to be used as predicate that may be given to `register_transform` + + First argument is a regular expression that will be searched against the `as_string` + representation of the node onto which it's applied. + + If specified, the second argument is an `attrgetter` expression that will be + applied on the node first to get the actual node on which `as_string` should + be called. + """ + def __init__(self, regexp, expression=None): + self.regexp = re.compile(regexp) + self.expression = expression + + def __call__(self, node): + if self.expression is not None: + node = attrgetter(self.expression)(node) + return self.regexp.search(node.as_string()) + +def inference_tip(infer_function): + """Given an instance specific inference function, return a function to be + given to MANAGER.register_transform to set this inference function. + + Typical usage + + .. sourcecode:: python + + MANAGER.register_transform(CallFunc, inference_tip(infer_named_tuple), + AsStringRegexpPredicate('namedtuple', 'func')) + """ + def transform(node, infer_function=infer_function): + node._explicit_inference = infer_function + return node + return transform + +# load brain plugins +# from os import listdir +# from os.path import join, dirname +# BRAIN_MODULES_DIR = join(dirname(__file__), 'brain') +# if BRAIN_MODULES_DIR not in sys.path: + # # add it to the end of the list so user path take precedence + # sys.path.append(BRAIN_MODULES_DIR) +# load modules in this directory +# for module in listdir(BRAIN_MODULES_DIR): + # if module.endswith('.py'): + # __import__(module[:-3]) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py b/pylibs/pylama/checkers/pylint/astroid/__pkginfo__.py similarity index 71% rename from pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py rename to pylibs/pylama/checkers/pylint/astroid/__pkginfo__.py index 31de45d0..a74b6b69 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/__pkginfo__.py +++ b/pylibs/pylama/checkers/pylint/astroid/__pkginfo__.py @@ -1,39 +1,37 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""logilab.astng packaging information""" +# with astroid. If not, see . +"""astroid packaging information""" -distname = 'logilab-astng' +distname = 'astroid' -modname = 'astng' -subpackage_of = 'logilab' +modname = 'astroid' -numversion = (0, 24, 3) +numversion = (1, 0, 0) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common >= 0.53.0'] +install_requires = ['logilab-common >= 0.60.0'] license = 'LGPL' author = 'Logilab' author_email = 'python-projects@lists.logilab.org' mailinglist = "mailto://%s" % author_email -web = "http://www.logilab.org/project/%s" % distname -ftp = "ftp://ftp.logilab.org/pub/%s" % modname +web = 'http://bitbucket.org/logilab/astroid' description = "rebuild a new abstract syntax tree from Python's ast" diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/as_string.py b/pylibs/pylama/checkers/pylint/astroid/as_string.py similarity index 78% rename from pylibs/pylama/checkers/pylint/logilab/astng/as_string.py rename to pylibs/pylama/checkers/pylint/astroid/as_string.py index c21144e5..fcff19eb 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/as_string.py +++ b/pylibs/pylama/checkers/pylint/astroid/as_string.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""This module renders ASTNG nodes as string: +# with astroid. If not, see . +"""This module renders Astroid nodes as string: * :func:`to_code` function return equivalent (hopefuly valid) python string @@ -29,7 +29,7 @@ def dump(node, ids=False): - """print a nice astng tree representation. + """print a nice astroid tree representation. :param ids: if true, we also print the ids (usefull for debugging) """ @@ -41,7 +41,7 @@ def _repr_tree(node, result, indent='', _done=None, ids=False): """built a tree representation of a node as a list of lines""" if _done is None: _done = set() - if not hasattr(node, '_astng_fields'): # not a astng node + if not hasattr(node, '_astroid_fields'): # not a astroid node return if node in _done: result.append( indent + 'loop in tree: %s' % node ) @@ -52,7 +52,7 @@ def _repr_tree(node, result, indent='', _done=None, ids=False): node_str += ' . \t%x' % id(node) result.append( indent + node_str ) indent += INDENT - for field in node._astng_fields: + for field in node._astroid_fields: value = getattr(node, field) if isinstance(value, (list, tuple) ): result.append( indent + field + " = [" ) @@ -71,7 +71,7 @@ def _repr_tree(node, result, indent='', _done=None, ids=False): class AsStringVisitor(object): - """Visitor to render an ASTNG node as a valid python code string""" + """Visitor to render an Astroid node as a valid python code string""" def __call__(self, node): """Makes this visitor behave as a simple function""" @@ -86,52 +86,52 @@ def _stmt_list(self, stmts): ## visit_ methods ########################################### def visit_arguments(self, node): - """return an astng.Function node as string""" + """return an astroid.Function node as string""" return node.format_args() def visit_assattr(self, node): - """return an astng.AssAttr node as string""" + """return an astroid.AssAttr node as string""" return self.visit_getattr(node) def visit_assert(self, node): - """return an astng.Assert node as string""" + """return an astroid.Assert node as string""" if node.fail: return 'assert %s, %s' % (node.test.accept(self), node.fail.accept(self)) return 'assert %s' % node.test.accept(self) def visit_assname(self, node): - """return an astng.AssName node as string""" + """return an astroid.AssName node as string""" return node.name def visit_assign(self, node): - """return an astng.Assign node as string""" + """return an astroid.Assign node as string""" lhs = ' = '.join([n.accept(self) for n in node.targets]) return '%s = %s' % (lhs, node.value.accept(self)) def visit_augassign(self, node): - """return an astng.AugAssign node as string""" + """return an astroid.AugAssign node as string""" return '%s %s %s' % (node.target.accept(self), node.op, node.value.accept(self)) def visit_backquote(self, node): - """return an astng.Backquote node as string""" + """return an astroid.Backquote node as string""" return '`%s`' % node.value.accept(self) def visit_binop(self, node): - """return an astng.BinOp node as string""" + """return an astroid.BinOp node as string""" return '(%s) %s (%s)' % (node.left.accept(self), node.op, node.right.accept(self)) def visit_boolop(self, node): - """return an astng.BoolOp node as string""" + """return an astroid.BoolOp node as string""" return (' %s ' % node.op).join(['(%s)' % n.accept(self) for n in node.values]) def visit_break(self, node): - """return an astng.Break node as string""" + """return an astroid.Break node as string""" return 'break' def visit_callfunc(self, node): - """return an astng.CallFunc node as string""" + """return an astroid.CallFunc node as string""" expr_str = node.func.accept(self) args = [arg.accept(self) for arg in node.args] if node.starargs: @@ -141,7 +141,7 @@ def visit_callfunc(self, node): return '%s(%s)' % (expr_str, ', '.join(args)) def visit_class(self, node): - """return an astng.Class node as string""" + """return an astroid.Class node as string""" decorate = node.decorators and node.decorators.accept(self) or '' bases = ', '.join([n.accept(self) for n in node.bases]) bases = bases and '(%s)' % bases or '' @@ -150,54 +150,54 @@ def visit_class(self, node): self._stmt_list( node.body)) def visit_compare(self, node): - """return an astng.Compare node as string""" + """return an astroid.Compare node as string""" rhs_str = ' '.join(['%s %s' % (op, expr.accept(self)) for op, expr in node.ops]) return '%s %s' % (node.left.accept(self), rhs_str) def visit_comprehension(self, node): - """return an astng.Comprehension node as string""" + """return an astroid.Comprehension node as string""" ifs = ''.join([ ' if %s' % n.accept(self) for n in node.ifs]) return 'for %s in %s%s' % (node.target.accept(self), node.iter.accept(self), ifs ) def visit_const(self, node): - """return an astng.Const node as string""" + """return an astroid.Const node as string""" return repr(node.value) def visit_continue(self, node): - """return an astng.Continue node as string""" + """return an astroid.Continue node as string""" return 'continue' def visit_delete(self, node): # XXX check if correct - """return an astng.Delete node as string""" + """return an astroid.Delete node as string""" return 'del %s' % ', '.join([child.accept(self) for child in node.targets]) def visit_delattr(self, node): - """return an astng.DelAttr node as string""" + """return an astroid.DelAttr node as string""" return self.visit_getattr(node) def visit_delname(self, node): - """return an astng.DelName node as string""" + """return an astroid.DelName node as string""" return node.name def visit_decorators(self, node): - """return an astng.Decorators node as string""" + """return an astroid.Decorators node as string""" return '@%s\n' % '\n@'.join([item.accept(self) for item in node.nodes]) def visit_dict(self, node): - """return an astng.Dict node as string""" + """return an astroid.Dict node as string""" return '{%s}' % ', '.join(['%s: %s' % (key.accept(self), value.accept(self)) for key, value in node.items]) def visit_dictcomp(self, node): - """return an astng.DictComp node as string""" + """return an astroid.DictComp node as string""" return '{%s: %s %s}' % (node.key.accept(self), node.value.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_discard(self, node): - """return an astng.Discard node as string""" + """return an astroid.Discard node as string""" return node.value.accept(self) def visit_emptynode(self, node): @@ -216,7 +216,7 @@ def visit_excepthandler(self, node): return '%s:\n%s' % (excs, self._stmt_list(node.body)) def visit_ellipsis(self, node): - """return an astng.Ellipsis node as string""" + """return an astroid.Ellipsis node as string""" return '...' def visit_empty(self, node): @@ -224,7 +224,7 @@ def visit_empty(self, node): return '' def visit_exec(self, node): - """return an astng.Exec node as string""" + """return an astroid.Exec node as string""" if node.locals: return 'exec %s in %s, %s' % (node.expr.accept(self), node.locals.accept(self), @@ -235,11 +235,11 @@ def visit_exec(self, node): return 'exec %s' % node.expr.accept(self) def visit_extslice(self, node): - """return an astng.ExtSlice node as string""" + """return an astroid.ExtSlice node as string""" return ','.join( [dim.accept(self) for dim in node.dims] ) def visit_for(self, node): - """return an astng.For node as string""" + """return an astroid.For node as string""" fors = 'for %s in %s:\n%s' % (node.target.accept(self), node.iter.accept(self), self._stmt_list( node.body)) @@ -248,78 +248,78 @@ def visit_for(self, node): return fors def visit_from(self, node): - """return an astng.From node as string""" + """return an astroid.From node as string""" return 'from %s import %s' % ('.' * (node.level or 0) + node.modname, _import_string(node.names)) def visit_function(self, node): - """return an astng.Function node as string""" + """return an astroid.Function node as string""" decorate = node.decorators and node.decorators.accept(self) or '' docs = node.doc and '\n%s"""%s"""' % (INDENT, node.doc) or '' return '\n%sdef %s(%s):%s\n%s' % (decorate, node.name, node.args.accept(self), docs, self._stmt_list(node.body)) def visit_genexpr(self, node): - """return an astng.GenExpr node as string""" + """return an astroid.GenExpr node as string""" return '(%s %s)' % (node.elt.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_getattr(self, node): - """return an astng.Getattr node as string""" + """return an astroid.Getattr node as string""" return '%s.%s' % (node.expr.accept(self), node.attrname) def visit_global(self, node): - """return an astng.Global node as string""" + """return an astroid.Global node as string""" return 'global %s' % ', '.join(node.names) def visit_if(self, node): - """return an astng.If node as string""" + """return an astroid.If node as string""" ifs = ['if %s:\n%s' % (node.test.accept(self), self._stmt_list(node.body))] if node.orelse:# XXX use elif ??? ifs.append('else:\n%s' % self._stmt_list(node.orelse)) return '\n'.join(ifs) def visit_ifexp(self, node): - """return an astng.IfExp node as string""" + """return an astroid.IfExp node as string""" return '%s if %s else %s' % (node.body.accept(self), node.test.accept(self), node.orelse.accept(self)) def visit_import(self, node): - """return an astng.Import node as string""" + """return an astroid.Import node as string""" return 'import %s' % _import_string(node.names) def visit_keyword(self, node): - """return an astng.Keyword node as string""" + """return an astroid.Keyword node as string""" return '%s=%s' % (node.arg, node.value.accept(self)) def visit_lambda(self, node): - """return an astng.Lambda node as string""" + """return an astroid.Lambda node as string""" return 'lambda %s: %s' % (node.args.accept(self), node.body.accept(self)) def visit_list(self, node): - """return an astng.List node as string""" + """return an astroid.List node as string""" return '[%s]' % ', '.join([child.accept(self) for child in node.elts]) def visit_listcomp(self, node): - """return an astng.ListComp node as string""" + """return an astroid.ListComp node as string""" return '[%s %s]' % (node.elt.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_module(self, node): - """return an astng.Module node as string""" + """return an astroid.Module node as string""" docs = node.doc and '"""%s"""\n\n' % node.doc or '' return docs + '\n'.join([n.accept(self) for n in node.body]) + '\n\n' def visit_name(self, node): - """return an astng.Name node as string""" + """return an astroid.Name node as string""" return node.name def visit_pass(self, node): - """return an astng.Pass node as string""" + """return an astroid.Pass node as string""" return 'pass' def visit_print(self, node): - """return an astng.Print node as string""" + """return an astroid.Print node as string""" nodes = ', '.join([n.accept(self) for n in node.values]) if not node.nl: nodes = '%s,' % nodes @@ -328,7 +328,7 @@ def visit_print(self, node): return 'print %s' % nodes def visit_raise(self, node): - """return an astng.Raise node as string""" + """return an astroid.Raise node as string""" if node.exc: if node.inst: if node.tback: @@ -341,27 +341,27 @@ def visit_raise(self, node): return 'raise' def visit_return(self, node): - """return an astng.Return node as string""" + """return an astroid.Return node as string""" if node.value: return 'return %s' % node.value.accept(self) else: return 'return' def visit_index(self, node): - """return a astng.Index node as string""" + """return a astroid.Index node as string""" return node.value.accept(self) def visit_set(self, node): - """return an astng.Set node as string""" + """return an astroid.Set node as string""" return '{%s}' % ', '.join([child.accept(self) for child in node.elts]) def visit_setcomp(self, node): - """return an astng.SetComp node as string""" + """return an astroid.SetComp node as string""" return '{%s %s}' % (node.elt.accept(self), ' '.join([n.accept(self) for n in node.generators])) def visit_slice(self, node): - """return a astng.Slice node as string""" + """return a astroid.Slice node as string""" lower = node.lower and node.lower.accept(self) or '' upper = node.upper and node.upper.accept(self) or '' step = node.step and node.step.accept(self) or '' @@ -370,11 +370,11 @@ def visit_slice(self, node): return '%s:%s' % (lower, upper) def visit_subscript(self, node): - """return an astng.Subscript node as string""" + """return an astroid.Subscript node as string""" return '%s[%s]' % (node.value.accept(self), node.slice.accept(self)) def visit_tryexcept(self, node): - """return an astng.TryExcept node as string""" + """return an astroid.TryExcept node as string""" trys = ['try:\n%s' % self._stmt_list( node.body)] for handler in node.handlers: trys.append(handler.accept(self)) @@ -383,16 +383,16 @@ def visit_tryexcept(self, node): return '\n'.join(trys) def visit_tryfinally(self, node): - """return an astng.TryFinally node as string""" + """return an astroid.TryFinally node as string""" return 'try:\n%s\nfinally:\n%s' % (self._stmt_list( node.body), self._stmt_list(node.finalbody)) def visit_tuple(self, node): - """return an astng.Tuple node as string""" + """return an astroid.Tuple node as string""" return '(%s)' % ', '.join([child.accept(self) for child in node.elts]) def visit_unaryop(self, node): - """return an astng.UnaryOp node as string""" + """return an astroid.UnaryOp node as string""" if node.op == 'not': operator = 'not ' else: @@ -400,7 +400,7 @@ def visit_unaryop(self, node): return '%s%s' % (operator, node.operand.accept(self)) def visit_while(self, node): - """return an astng.While node as string""" + """return an astroid.While node as string""" whiles = 'while %s:\n%s' % (node.test.accept(self), self._stmt_list(node.body)) if node.orelse: @@ -408,11 +408,11 @@ def visit_while(self, node): return whiles def visit_with(self, node): # 'with' without 'as' is possible - """return an astng.With node as string""" - as_var = node.vars and " as (%s)" % (node.vars.accept(self)) or "" - withs = 'with (%s)%s:\n%s' % (node.expr.accept(self), as_var, - self._stmt_list( node.body)) - return withs + """return an astroid.With node as string""" + items = ', '.join(('(%s)' % expr.accept(self)) + + (vars and ' as (%s)' % (vars.accept(self)) or '') + for expr, vars in node.items) + return 'with %s:\n%s' % (items, self._stmt_list( node.body)) def visit_yield(self, node): """yield an ast.Yield node as string""" @@ -439,11 +439,11 @@ def visit_excepthandler(self, node): return '%s:\n%s' % (excs, self._stmt_list(node.body)) def visit_nonlocal(self, node): - """return an astng.Nonlocal node as string""" + """return an astroid.Nonlocal node as string""" return 'nonlocal %s' % ', '.join(node.names) def visit_raise(self, node): - """return an astng.Raise node as string""" + """return an astroid.Raise node as string""" if node.exc: if node.cause: return 'raise %s from %s' % (node.exc.accept(self), diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/bases.py b/pylibs/pylama/checkers/pylint/astroid/bases.py similarity index 92% rename from pylibs/pylama/checkers/pylint/logilab/astng/bases.py rename to pylibs/pylama/checkers/pylint/astroid/bases.py index 6331dc8e..641f88ae 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/bases.py +++ b/pylibs/pylama/checkers/pylint/astroid/bases.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """This module contains base classes and functions for the nodes and some inference utils. """ @@ -24,8 +24,8 @@ import sys from contextlib import contextmanager -from .exceptions import (InferenceError, ASTNGError, - NotFoundError, UnresolvableName) +from .exceptions import (InferenceError, AstroidError, NotFoundError, + UnresolvableName, UseInferenceDefault) if sys.version_info >= (3, 0): @@ -339,7 +339,7 @@ def wrapper(*args, **kwargs): # Node ###################################################################### class NodeNG(object): - """Base Class for all ASTNG node classes. + """Base Class for all Astroid node classes. It represents a node of the new abstract syntax tree. """ @@ -354,7 +354,24 @@ class NodeNG(object): # parent node in the tree parent = None # attributes containing child node(s) redefined in most concrete classes: - _astng_fields = () + _astroid_fields = () + # instance specific inference function infer(node, context) + _explicit_inference = None + + def infer(self, context=None, **kwargs): + """main interface to the interface system, return a generator on infered + values. + + If the instance has some explicit inference function set, it will be + called instead of the default interface. + """ + if self._explicit_inference is not None: + # explicit_inference is not bound, give it self explicitly + try: + return self._explicit_inference(self, context, **kwargs) + except UseInferenceDefault: + pass + return self._infer(context, **kwargs) def _repr_name(self): """return self.name or self.attrname or '' for nice representation""" @@ -377,7 +394,7 @@ def accept(self, visitor): return func(self) def get_children(self): - for field in self._astng_fields: + for field in self._astroid_fields: attr = getattr(self, field) if attr is None: continue @@ -389,7 +406,7 @@ def get_children(self): def last_child(self): """an optimized version of list(get_children())[-1]""" - for field in self._astng_fields[::-1]: + for field in self._astroid_fields[::-1]: attr = getattr(self, field) if not attr: # None or empty listy / tuple continue @@ -433,7 +450,7 @@ def root(self): def child_sequence(self, child): """search for the right sequence where the child lies in""" - for field in self._astng_fields: + for field in self._astroid_fields: node_or_sequence = getattr(self, field) if node_or_sequence is child: return [node_or_sequence] @@ -441,20 +458,20 @@ def child_sequence(self, child): if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence: return node_or_sequence else: - msg = 'Could not found %s in %s\'s children' - raise ASTNGError(msg % (repr(child), repr(self))) + msg = 'Could not find %s in %s\'s children' + raise AstroidError(msg % (repr(child), repr(self))) def locate_child(self, child): """return a 2-uple (child attribute name, sequence or node)""" - for field in self._astng_fields: + for field in self._astroid_fields: node_or_sequence = getattr(self, field) # /!\ compiler.ast Nodes have an __iter__ walking over child nodes if child is node_or_sequence: return field, child if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence: return field, node_or_sequence - msg = 'Could not found %s in %s\'s children' - raise ASTNGError(msg % (repr(child), repr(self))) + msg = 'Could not find %s in %s\'s children' + raise AstroidError(msg % (repr(child), repr(self))) # FIXME : should we merge child_sequence and locate_child ? locate_child # is only used in are_exclusive, child_sequence one time in pylint. @@ -543,7 +560,7 @@ def _infer_name(self, frame, name): # overridden for From, Import, Global, TryExcept and Arguments return None - def infer(self, context=None): + def _infer(self, context=None): """we don't know how to resolve a statement by default""" # this method is overridden by most concrete classes raise InferenceError(self.__class__.__name__) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/builder.py b/pylibs/pylama/checkers/pylint/astroid/builder.py similarity index 83% rename from pylibs/pylama/checkers/pylint/logilab/astng/builder.py rename to pylibs/pylama/checkers/pylint/astroid/builder.py index ae0c7d01..6a4347af 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/builder.py +++ b/pylibs/pylama/checkers/pylint/astroid/builder.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""The ASTNGBuilder makes astng from living object and / or from _ast +# with astroid. If not, see . +"""The AstroidBuilder makes astroid from living object and / or from _ast The builder is not thread safe and can't be used to parse different sources at the same time. @@ -26,12 +26,12 @@ import sys from os.path import splitext, basename, exists, abspath -from ..common.modutils import modpath_from_file +from ..logilab.common.modutils import modpath_from_file -from .exceptions import ASTNGBuildingException, InferenceError +from .exceptions import AstroidBuildingException, InferenceError from .raw_building import InspectBuilder from .rebuilder import TreeRebuilder -from .manager import ASTNGManager +from .manager import AstroidManager from .bases import YES, Instance from _ast import PyCF_ONLY_AST @@ -50,7 +50,7 @@ def open_source_file(filename): except UnicodeError, uex: # wrong encodingg # detect_encoding returns utf-8 if no encoding specified msg = 'Wrong (%s) or no encoding specified' % encoding - raise ASTNGBuildingException(msg) + raise AstroidBuildingException(msg) return stream, encoding, data else: @@ -79,18 +79,17 @@ def open_source_file(filename): # ast NG builder ############################################################## -MANAGER = ASTNGManager() +MANAGER = AstroidManager() -class ASTNGBuilder(InspectBuilder): - """provide astng building methods""" - rebuilder = TreeRebuilder() +class AstroidBuilder(InspectBuilder): + """provide astroid building methods""" def __init__(self, manager=None): InspectBuilder.__init__(self) self._manager = manager or MANAGER def module_build(self, module, modname=None): - """build an astng from a living module instance + """build an astroid from a living module instance """ node = None path = getattr(module, '__file__', None) @@ -105,7 +104,7 @@ def module_build(self, module, modname=None): return node def file_build(self, path, modname=None): - """build astng from a source code file (i.e. from an ast) + """build astroid from a source code file (i.e. from an ast) path is expected to be a python source file """ @@ -113,35 +112,32 @@ def file_build(self, path, modname=None): stream, encoding, data = open_source_file(path) except IOError, exc: msg = 'Unable to load file %r (%s)' % (path, exc) - raise ASTNGBuildingException(msg) + raise AstroidBuildingException(msg) except SyntaxError, exc: # py3k encoding specification error - raise ASTNGBuildingException(exc) + raise AstroidBuildingException(exc) except LookupError, exc: # unknown encoding - raise ASTNGBuildingException(exc) + raise AstroidBuildingException(exc) # get module name if necessary if modname is None: try: modname = '.'.join(modpath_from_file(path)) except ImportError: modname = splitext(basename(path))[0] - # build astng representation + # build astroid representation node = self.string_build(data, modname, path) node.file_encoding = encoding return node def string_build(self, data, modname='', path=None): - """build astng from source code string and return rebuilded astng""" + """build astroid from source code string and return rebuilded astroid""" module = self._data_build(data, modname, path) - self._manager.astng_cache[module.name] = module + self._manager.astroid_cache[module.name] = module # post tree building steps after we stored the module in the cache: for from_node in module._from_nodes: self.add_from_names_to_locals(from_node) # handle delayed assattr nodes for delayed in module._delayed_assattr: self.delayed_assattr(delayed) - if modname: - for transformer in self._manager.transformers: - transformer(module) return module def _data_build(self, data, modname, path): @@ -157,11 +153,11 @@ def _data_build(self, data, modname, path): package = True else: package = path and path.find('__init__.py') > -1 or False - self.rebuilder.init() - module = self.rebuilder.visit_module(node, modname, package) + rebuilder = TreeRebuilder(self._manager) + module = rebuilder.visit_module(node, modname, package) module.file = module.path = node_file - module._from_nodes = self.rebuilder._from_nodes - module._delayed_assattr = self.rebuilder._delayed_assattr + module._from_nodes = rebuilder._from_nodes + module._delayed_assattr = rebuilder._delayed_assattr return module def add_from_names_to_locals(self, node): @@ -176,7 +172,7 @@ def sort_locals(my_list): if name == '*': try: imported = node.root().import_module(node.modname) - except ASTNGBuildingException: + except AstroidBuildingException: continue for name in imported.wildcard_import_names(): node.parent.set_local(name, node) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py b/pylibs/pylama/checkers/pylint/astroid/exceptions.py similarity index 55% rename from pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py rename to pylibs/pylama/checkers/pylint/astroid/exceptions.py index db33f8b9..3889e2e7 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/exceptions.py +++ b/pylibs/pylama/checkers/pylint/astroid/exceptions.py @@ -1,34 +1,34 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""this module contains exceptions used in the astng library +# with astroid. If not, see . +"""this module contains exceptions used in the astroid library """ __doctype__ = "restructuredtext en" -class ASTNGError(Exception): - """base exception class for all astng related exceptions""" +class AstroidError(Exception): + """base exception class for all astroid related exceptions""" -class ASTNGBuildingException(ASTNGError): - """exception class when we are unable to build an astng representation""" +class AstroidBuildingException(AstroidError): + """exception class when we are unable to build an astroid representation""" -class ResolveError(ASTNGError): - """base class of astng resolution/inference error""" +class ResolveError(AstroidError): + """base class of astroid resolution/inference error""" class NotFoundError(ResolveError): """raised when we are unable to resolve a name""" @@ -36,10 +36,15 @@ class NotFoundError(ResolveError): class InferenceError(ResolveError): """raised when we are unable to infer a node""" +class UseInferenceDefault(Exception): + """exception to be raised in custom inference function to indicate that it + should go back to the default behaviour + """ + class UnresolvableName(InferenceError): """raised when we are unable to resolve a name""" -class NoDefault(ASTNGError): +class NoDefault(AstroidError): """raised by function's `default_value` method when an argument has no default value """ diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/inference.py b/pylibs/pylama/checkers/pylint/astroid/inference.py similarity index 88% rename from pylibs/pylama/checkers/pylint/logilab/astng/inference.py rename to pylibs/pylama/checkers/pylint/astroid/inference.py index fda25cf8..f600781c 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/inference.py +++ b/pylibs/pylama/checkers/pylint/astroid/inference.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""this module contains a set of functions to handle inference on astng trees +# with astroid. If not, see . +"""this module contains a set of functions to handle inference on astroid trees """ __doctype__ = "restructuredtext en" @@ -24,14 +24,14 @@ from . import nodes -from .manager import ASTNGManager -from .exceptions import (ASTNGError, +from .manager import AstroidManager +from .exceptions import (AstroidError, InferenceError, NoDefault, NotFoundError, UnresolvableName) from .bases import YES, Instance, InferenceContext, \ _infer_stmts, copy_context, path_wrapper, raise_if_nothing_infered from .protocols import _arguments_infer_argname -MANAGER = ASTNGManager() +MANAGER = AstroidManager() class CallContext: @@ -55,7 +55,7 @@ def infer_argument(self, funcnode, name, context): try: return self.nargs[name].infer(context) except KeyError: - # Function.args.args can be None in astng (means that we don't have + # Function.args.args can be None in astroid (means that we don't have # information on argnames) argindex = funcnode.args.find_argname(name)[0] if argindex is not None: @@ -126,15 +126,15 @@ def infer_end(self, context=None): """inference's end for node such as Module, Class, Function, Const... """ yield self -nodes.Module.infer = infer_end -nodes.Class.infer = infer_end -nodes.Function.infer = infer_end -nodes.Lambda.infer = infer_end -nodes.Const.infer = infer_end -nodes.List.infer = infer_end -nodes.Tuple.infer = infer_end -nodes.Dict.infer = infer_end -nodes.Set.infer = infer_end +nodes.Module._infer = infer_end +nodes.Class._infer = infer_end +nodes.Function._infer = infer_end +nodes.Lambda._infer = infer_end +nodes.Const._infer = infer_end +nodes.List._infer = infer_end +nodes.Tuple._infer = infer_end +nodes.Dict._infer = infer_end +nodes.Set._infer = infer_end def infer_name(self, context=None): """infer a Name: use name lookup rules""" @@ -144,7 +144,7 @@ def infer_name(self, context=None): context = context.clone() context.lookupname = self.name return _infer_stmts(stmts, context, frame) -nodes.Name.infer = path_wrapper(infer_name) +nodes.Name._infer = path_wrapper(infer_name) nodes.AssName.infer_lhs = infer_name # won't work with a path wrapper @@ -164,7 +164,7 @@ def infer_callfunc(self, context=None): except InferenceError: ## XXX log error ? continue -nodes.CallFunc.infer = path_wrapper(raise_if_nothing_infered(infer_callfunc)) +nodes.CallFunc._infer = path_wrapper(raise_if_nothing_infered(infer_callfunc)) def infer_import(self, context=None, asname=True): @@ -176,7 +176,7 @@ def infer_import(self, context=None, asname=True): yield self.do_import_module(self.real_name(name)) else: yield self.do_import_module(name) -nodes.Import.infer = path_wrapper(infer_import) +nodes.Import._infer = path_wrapper(infer_import) def infer_name_module(self, name): context = InferenceContext() @@ -199,7 +199,7 @@ def infer_from(self, context=None, asname=True): return _infer_stmts(module.getattr(name, ignore_locals=module is self.root()), context) except NotFoundError: raise InferenceError(name) -nodes.From.infer = path_wrapper(infer_from) +nodes.From._infer = path_wrapper(infer_from) def infer_getattr(self, context=None): @@ -219,7 +219,7 @@ def infer_getattr(self, context=None): except AttributeError: # XXX method / function context.boundnode = None -nodes.Getattr.infer = path_wrapper(raise_if_nothing_infered(infer_getattr)) +nodes.Getattr._infer = path_wrapper(raise_if_nothing_infered(infer_getattr)) nodes.AssAttr.infer_lhs = raise_if_nothing_infered(infer_getattr) # # won't work with a path wrapper @@ -230,7 +230,7 @@ def infer_global(self, context=None): return _infer_stmts(self.root().getattr(context.lookupname), context) except NotFoundError: raise InferenceError() -nodes.Global.infer = path_wrapper(infer_global) +nodes.Global._infer = path_wrapper(infer_global) def infer_subscript(self, context=None): @@ -257,7 +257,7 @@ def infer_subscript(self, context=None): yield infered else: raise InferenceError() -nodes.Subscript.infer = path_wrapper(infer_subscript) +nodes.Subscript._infer = path_wrapper(infer_subscript) nodes.Subscript.infer_lhs = raise_if_nothing_infered(infer_subscript) @@ -287,7 +287,7 @@ def infer_unaryop(self, context=None): raise except: yield YES -nodes.UnaryOp.infer = path_wrapper(infer_unaryop) +nodes.UnaryOp._infer = path_wrapper(infer_unaryop) BIN_OP_METHOD = {'+': '__add__', @@ -332,7 +332,7 @@ def infer_binop(self, context=None): for rhs in self.right.infer(context): for val in _infer_binop(self.op, rhs, lhs, context): yield val -nodes.BinOp.infer = path_wrapper(infer_binop) +nodes.BinOp._infer = path_wrapper(infer_binop) def infer_arguments(self, context=None): @@ -340,7 +340,7 @@ def infer_arguments(self, context=None): if name is None: raise InferenceError() return _arguments_infer_argname(self, name, context) -nodes.Arguments.infer = infer_arguments +nodes.Arguments._infer = infer_arguments def infer_ass(self, context=None): @@ -352,8 +352,8 @@ def infer_ass(self, context=None): return stmt.infer(context) stmts = list(self.assigned_stmts(context=context)) return _infer_stmts(stmts, context) -nodes.AssName.infer = path_wrapper(infer_ass) -nodes.AssAttr.infer = path_wrapper(infer_ass) +nodes.AssName._infer = path_wrapper(infer_ass) +nodes.AssAttr._infer = path_wrapper(infer_ass) def infer_augassign(self, context=None): failures = [] @@ -364,7 +364,7 @@ def infer_augassign(self, context=None): for rhs in self.value.infer(context): for val in _infer_binop(self.op, rhs, lhs, context): yield val -nodes.AugAssign.infer = path_wrapper(infer_augassign) +nodes.AugAssign._infer = path_wrapper(infer_augassign) # no infer method on DelName and DelAttr (expected InferenceError) @@ -375,14 +375,14 @@ def infer_empty_node(self, context=None): yield YES else: try: - for infered in MANAGER.infer_astng_from_something(self.object, + for infered in MANAGER.infer_ast_from_something(self.object, context=context): yield infered - except ASTNGError: + except AstroidError: yield YES -nodes.EmptyNode.infer = path_wrapper(infer_empty_node) +nodes.EmptyNode._infer = path_wrapper(infer_empty_node) def infer_index(self, context=None): return self.value.infer(context) -nodes.Index.infer = infer_index +nodes.Index._infer = infer_index diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/manager.py b/pylibs/pylama/checkers/pylint/astroid/manager.py similarity index 63% rename from pylibs/pylama/checkers/pylint/logilab/astng/manager.py rename to pylibs/pylama/checkers/pylint/astroid/manager.py index d090f80b..3ff2e14c 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/manager.py +++ b/pylibs/pylama/checkers/pylint/astroid/manager.py @@ -1,22 +1,22 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""astng manager: avoid multiple astng build of a same module when -possible by providing a class responsible to get astng representation +# with astroid. If not, see . +"""astroid manager: avoid multiple astroid build of a same module when +possible by providing a class responsible to get astroid representation from various source and using a cache of built modules) """ @@ -25,19 +25,19 @@ import os from os.path import dirname, join, isdir, exists -from ..common.modutils import NoSourceFile, is_python_source, \ +from ..logilab.common.modutils import NoSourceFile, is_python_source, \ file_from_modpath, load_module_from_name, modpath_from_file, \ get_module_files, get_source_file, zipimport -from ..common.configuration import OptionsProviderMixIn +from ..logilab.common.configuration import OptionsProviderMixIn -from .exceptions import ASTNGBuildingException +from .exceptions import AstroidBuildingException -def astng_wrapper(func, modname): - """wrapper to give to ASTNGManager.project_from_files""" +def astroid_wrapper(func, modname): + """wrapper to give to AstroidManager.project_from_files""" print 'parsing %s...' % modname try: return func(modname) - except ASTNGBuildingException, exc: + except AstroidBuildingException, exc: print exc except Exception, exc: import traceback @@ -55,14 +55,14 @@ def safe_repr(obj): -class ASTNGManager(OptionsProviderMixIn): - """the astng manager, responsible to build astng from files +class AstroidManager(OptionsProviderMixIn): + """the astroid manager, responsible to build astroid from files or modules. Use the Borg pattern. """ - name = 'astng loader' + name = 'astroid loader' options = (("ignore", {'type' : "csv", 'metavar' : "", 'dest' : "black_list", "default" : ('CVS',), @@ -76,17 +76,17 @@ class ASTNGManager(OptionsProviderMixIn): ) brain = {} def __init__(self): - self.__dict__ = ASTNGManager.brain + self.__dict__ = AstroidManager.brain if not self.__dict__: OptionsProviderMixIn.__init__(self) self.load_defaults() # NOTE: cache entries are added by the [re]builder - self.astng_cache = {} + self.astroid_cache = {} self._mod_file_cache = {} - self.transformers = [] + self.transforms = {} - def astng_from_file(self, filepath, modname=None, fallback=True, source=False): - """given a module name, return the astng object""" + def ast_from_file(self, filepath, modname=None, fallback=True, source=False): + """given a module name, return the astroid object""" try: filepath = get_source_file(filepath, include_no_ext=True) source = True @@ -97,23 +97,23 @@ def astng_from_file(self, filepath, modname=None, fallback=True, source=False): modname = '.'.join(modpath_from_file(filepath)) except ImportError: modname = filepath - if modname in self.astng_cache: - return self.astng_cache[modname] + if modname in self.astroid_cache: + return self.astroid_cache[modname] if source: - from .builder import ASTNGBuilder - return ASTNGBuilder(self).file_build(filepath, modname) + from .builder import AstroidBuilder + return AstroidBuilder(self).file_build(filepath, modname) elif fallback and modname: - return self.astng_from_module_name(modname) - raise ASTNGBuildingException('unable to get astng for file %s' % + return self.ast_from_module_name(modname) + raise AstroidBuildingException('unable to get astroid for file %s' % filepath) - def astng_from_module_name(self, modname, context_file=None): - """given a module name, return the astng object""" - if modname in self.astng_cache: - return self.astng_cache[modname] + def ast_from_module_name(self, modname, context_file=None): + """given a module name, return the astroid object""" + if modname in self.astroid_cache: + return self.astroid_cache[modname] if modname == '__main__': - from .builder import ASTNGBuilder - return ASTNGBuilder(self).string_build('', modname) + from .builder import AstroidBuilder + return AstroidBuilder(self).string_build('', modname) old_cwd = os.getcwd() if context_file: os.chdir(dirname(context_file)) @@ -128,17 +128,17 @@ def astng_from_module_name(self, modname, context_file=None): module = load_module_from_name(modname) except Exception, ex: msg = 'Unable to load module %s (%s)' % (modname, ex) - raise ASTNGBuildingException(msg) - return self.astng_from_module(module, modname) - return self.astng_from_file(filepath, modname, fallback=False) + raise AstroidBuildingException(msg) + return self.ast_from_module(module, modname) + return self.ast_from_file(filepath, modname, fallback=False) finally: os.chdir(old_cwd) def zip_import_data(self, filepath): if zipimport is None: return None - from .builder import ASTNGBuilder - builder = ASTNGBuilder(self) + from .builder import AstroidBuilder + builder = AstroidBuilder(self) for ext in ('.zip', '.egg'): try: eggpath, resource = filepath.rsplit(ext + '/', 1) @@ -165,41 +165,41 @@ def file_from_module_name(self, modname, contextfile): context_file=contextfile) except ImportError, ex: msg = 'Unable to load module %s (%s)' % (modname, ex) - value = ASTNGBuildingException(msg) + value = AstroidBuildingException(msg) self._mod_file_cache[(modname, contextfile)] = value - if isinstance(value, ASTNGBuildingException): + if isinstance(value, AstroidBuildingException): raise value return value - def astng_from_module(self, module, modname=None): - """given an imported module, return the astng object""" + def ast_from_module(self, module, modname=None): + """given an imported module, return the astroid object""" modname = modname or module.__name__ - if modname in self.astng_cache: - return self.astng_cache[modname] + if modname in self.astroid_cache: + return self.astroid_cache[modname] try: # some builtin modules don't have __file__ attribute filepath = module.__file__ if is_python_source(filepath): - return self.astng_from_file(filepath, modname) + return self.ast_from_file(filepath, modname) except AttributeError: pass - from .builder import ASTNGBuilder - return ASTNGBuilder(self).module_build(module, modname) + from .builder import AstroidBuilder + return AstroidBuilder(self).module_build(module, modname) - def astng_from_class(self, klass, modname=None): - """get astng for the given class""" + def ast_from_class(self, klass, modname=None): + """get astroid for the given class""" if modname is None: try: modname = klass.__module__ except AttributeError: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unable to get module for class %s' % safe_repr(klass)) - modastng = self.astng_from_module_name(modname) - return modastng.getattr(klass.__name__)[0] # XXX + modastroid = self.ast_from_module_name(modname) + return modastroid.getattr(klass.__name__)[0] # XXX - def infer_astng_from_something(self, obj, context=None): - """infer astng for the given class""" + def infer_ast_from_something(self, obj, context=None): + """infer astroid for the given class""" if hasattr(obj, '__class__') and not isinstance(obj, type): klass = obj.__class__ else: @@ -207,31 +207,31 @@ def infer_astng_from_something(self, obj, context=None): try: modname = klass.__module__ except AttributeError: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unable to get module for %s' % safe_repr(klass)) except Exception, ex: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unexpected error while retrieving module for %s: %s' % (safe_repr(klass), ex)) try: name = klass.__name__ except AttributeError: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unable to get name for %s' % safe_repr(klass)) except Exception, ex: - raise ASTNGBuildingException( + raise AstroidBuildingException( 'Unexpected error while retrieving name for %s: %s' % (safe_repr(klass), ex)) # take care, on living object __module__ is regularly wrong :( - modastng = self.astng_from_module_name(modname) + modastroid = self.ast_from_module_name(modname) if klass is obj: - for infered in modastng.igetattr(name, context): + for infered in modastroid.igetattr(name, context): yield infered else: - for infered in modastng.igetattr(name, context): + for infered in modastroid.igetattr(name, context): yield infered.instanciate_class() - def project_from_files(self, files, func_wrapper=astng_wrapper, + def project_from_files(self, files, func_wrapper=astroid_wrapper, project_name=None, black_list=None): """return a Project from a list of files or modules""" # build the project representation @@ -245,26 +245,33 @@ def project_from_files(self, files, func_wrapper=astng_wrapper, fpath = join(something, '__init__.py') else: fpath = something - astng = func_wrapper(self.astng_from_file, fpath) - if astng is None: + astroid = func_wrapper(self.ast_from_file, fpath) + if astroid is None: continue # XXX why is first file defining the project.path ? - project.path = project.path or astng.file - project.add_module(astng) - base_name = astng.name + project.path = project.path or astroid.file + project.add_module(astroid) + base_name = astroid.name # recurse in package except if __init__ was explicitly given - if astng.package and something.find('__init__') == -1: + if astroid.package and something.find('__init__') == -1: # recurse on others packages / modules if this is a package - for fpath in get_module_files(dirname(astng.file), + for fpath in get_module_files(dirname(astroid.file), black_list): - astng = func_wrapper(self.astng_from_file, fpath) - if astng is None or astng.name == base_name: + astroid = func_wrapper(self.ast_from_file, fpath) + if astroid is None or astroid.name == base_name: continue - project.add_module(astng) + project.add_module(astroid) return project - def register_transformer(self, transformer): - self.transformers.append(transformer) + def register_transform(self, node_class, transform, predicate=None): + """Register `transform(node)` function to be applied on the given + Astroid's `node_class` if `predicate` is None or return a true value + when called with the node as argument. + + The transform function may return a value which is then used to + substitute the original node in the tree. + """ + self.transforms.setdefault(node_class, []).append( (transform, predicate) ) class Project: """a project handle a set of modules / packages""" diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/mixins.py b/pylibs/pylama/checkers/pylint/astroid/mixins.py similarity index 91% rename from pylibs/pylama/checkers/pylint/logilab/astng/mixins.py rename to pylibs/pylama/checkers/pylint/astroid/mixins.py index 5d4a865c..42bbb404 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/mixins.py +++ b/pylibs/pylama/checkers/pylint/astroid/mixins.py @@ -1,24 +1,24 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """This module contains some mixins for the different nodes. """ -from .exceptions import (ASTNGBuildingException, InferenceError, +from .exceptions import (AstroidBuildingException, InferenceError, NotFoundError) @@ -101,7 +101,7 @@ def do_import_module(self, modname): return mymodule try: return mymodule.import_module(modname, level=level) - except ASTNGBuildingException: + except AstroidBuildingException: raise InferenceError(modname) except SyntaxError, ex: raise InferenceError(str(ex)) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py b/pylibs/pylama/checkers/pylint/astroid/node_classes.py similarity index 89% rename from pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py rename to pylibs/pylama/checkers/pylint/astroid/node_classes.py index a7976774..52cd0a27 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/node_classes.py +++ b/pylibs/pylama/checkers/pylint/astroid/node_classes.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """Module for some node classes. More nodes in scoped_nodes.py """ @@ -253,9 +253,11 @@ class Name(LookupMixIn, NodeNG): class Arguments(NodeNG, AssignTypeMixin): """class representing an Arguments node""" - _astng_fields = ('args', 'defaults') + _astroid_fields = ('args', 'defaults', 'kwonlyargs', 'kw_defaults') args = None defaults = None + kwonlyargs = None + kw_defaults = None def __init__(self, vararg=None, kwarg=None): self.vararg = vararg @@ -268,11 +270,17 @@ def _infer_name(self, frame, name): def format_args(self): """return arguments formatted as string""" - result = [_format_args(self.args, self.defaults)] + result = [] + if self.args: + result.append(_format_args(self.args, self.defaults)) if self.vararg: result.append('*%s' % self.vararg) if self.kwarg: result.append('**%s' % self.kwarg) + if self.kwonlyargs: + if not self.vararg: + result.append('*') + result.append(_format_args(self.kwonlyargs, self.kw_defaults)) return ', '.join(result) def default_value(self, argname): @@ -285,6 +293,9 @@ def default_value(self, argname): idx = i - (len(self.args) - len(self.defaults)) if idx >= 0: return self.defaults[idx] + i = _find_arg(argname, self.kwonlyargs)[0] + if i is not None and self.kw_defaults[i] is not None: + return self.kw_defaults[i] raise NoDefault() def is_argument(self, name): @@ -301,6 +312,12 @@ def find_argname(self, argname, rec=False): return _find_arg(argname, self.args, rec) return None, None + def get_children(self): + """override get_children to skip over None elements in kw_defaults""" + for child in super(Arguments, self).get_children(): + if child is not None: + yield child + def _find_arg(argname, args, rec=False): for i, arg in enumerate(args): @@ -326,47 +343,48 @@ def _format_args(args, defaults=None): else: values.append(arg.name) if defaults is not None and i >= default_offset: - values[-1] += '=' + defaults[i-default_offset].as_string() + if defaults[i-default_offset] is not None: + values[-1] += '=' + defaults[i-default_offset].as_string() return ', '.join(values) class AssAttr(NodeNG, ParentAssignTypeMixin): """class representing an AssAttr node""" - _astng_fields = ('expr',) + _astroid_fields = ('expr',) expr = None class Assert(Statement): """class representing an Assert node""" - _astng_fields = ('test', 'fail',) + _astroid_fields = ('test', 'fail',) test = None fail = None class Assign(Statement, AssignTypeMixin): """class representing an Assign node""" - _astng_fields = ('targets', 'value',) + _astroid_fields = ('targets', 'value',) targets = None value = None class AugAssign(Statement, AssignTypeMixin): """class representing an AugAssign node""" - _astng_fields = ('target', 'value',) + _astroid_fields = ('target', 'value',) target = None value = None class Backquote(NodeNG): """class representing a Backquote node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class BinOp(NodeNG): """class representing a BinOp node""" - _astng_fields = ('left', 'right',) + _astroid_fields = ('left', 'right',) left = None right = None class BoolOp(NodeNG): """class representing a BoolOp node""" - _astng_fields = ('values',) + _astroid_fields = ('values',) values = None class Break(Statement): @@ -375,7 +393,7 @@ class Break(Statement): class CallFunc(NodeNG): """class representing a CallFunc node""" - _astng_fields = ('func', 'args', 'starargs', 'kwargs') + _astroid_fields = ('func', 'args', 'starargs', 'kwargs') func = None args = None starargs = None @@ -387,7 +405,7 @@ def __init__(self): class Compare(NodeNG): """class representing a Compare node""" - _astng_fields = ('left', 'ops',) + _astroid_fields = ('left', 'ops',) left = None ops = None @@ -405,7 +423,7 @@ def last_child(self): class Comprehension(NodeNG): """class representing a Comprehension node""" - _astng_fields = ('target', 'iter' ,'ifs') + _astroid_fields = ('target', 'iter' ,'ifs') target = None iter = None ifs = None @@ -458,7 +476,7 @@ class Continue(Statement): class Decorators(NodeNG): """class representing a Decorators node""" - _astng_fields = ('nodes',) + _astroid_fields = ('nodes',) nodes = None def __init__(self, nodes=None): @@ -470,19 +488,19 @@ def scope(self): class DelAttr(NodeNG, ParentAssignTypeMixin): """class representing a DelAttr node""" - _astng_fields = ('expr',) + _astroid_fields = ('expr',) expr = None class Delete(Statement, AssignTypeMixin): """class representing a Delete node""" - _astng_fields = ('targets',) + _astroid_fields = ('targets',) targets = None class Dict(NodeNG, Instance): """class representing a Dict node""" - _astng_fields = ('items',) + _astroid_fields = ('items',) def __init__(self, items=None): if items is None: @@ -524,7 +542,7 @@ def getitem(self, lookup_key, context=None): class Discard(Statement): """class representing a Discard node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None @@ -538,7 +556,7 @@ class EmptyNode(NodeNG): class ExceptHandler(Statement, AssignTypeMixin): """class representing an ExceptHandler node""" - _astng_fields = ('type', 'name', 'body',) + _astroid_fields = ('type', 'name', 'body',) type = None name = None body = None @@ -566,7 +584,7 @@ def catch(self, exceptions): class Exec(Statement): """class representing an Exec node""" - _astng_fields = ('expr', 'globals', 'locals',) + _astroid_fields = ('expr', 'globals', 'locals',) expr = None globals = None locals = None @@ -574,12 +592,12 @@ class Exec(Statement): class ExtSlice(NodeNG): """class representing an ExtSlice node""" - _astng_fields = ('dims',) + _astroid_fields = ('dims',) dims = None class For(BlockRangeMixIn, AssignTypeMixin, Statement): """class representing a For node""" - _astng_fields = ('target', 'iter', 'body', 'orelse',) + _astroid_fields = ('target', 'iter', 'body', 'orelse',) target = None iter = None body = None @@ -600,7 +618,7 @@ def __init__(self, fromname, names, level=0): class Getattr(NodeNG): """class representing a Getattr node""" - _astng_fields = ('expr',) + _astroid_fields = ('expr',) expr = None @@ -616,7 +634,7 @@ def _infer_name(self, frame, name): class If(BlockRangeMixIn, Statement): """class representing an If node""" - _astng_fields = ('test', 'body', 'orelse') + _astroid_fields = ('test', 'body', 'orelse') test = None body = None orelse = None @@ -636,7 +654,7 @@ def block_range(self, lineno): class IfExp(NodeNG): """class representing an IfExp node""" - _astng_fields = ('test', 'body', 'orelse') + _astroid_fields = ('test', 'body', 'orelse') test = None body = None orelse = None @@ -648,19 +666,19 @@ class Import(FromImportMixIn, Statement): class Index(NodeNG): """class representing an Index node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class Keyword(NodeNG): """class representing a Keyword node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class List(NodeNG, Instance, ParentAssignTypeMixin): """class representing a List node""" - _astng_fields = ('elts',) + _astroid_fields = ('elts',) def __init__(self, elts=None): if elts is None: @@ -694,7 +712,7 @@ class Pass(Statement): class Print(Statement): """class representing a Print node""" - _astng_fields = ('dest', 'values',) + _astroid_fields = ('dest', 'values',) dest = None values = None @@ -703,11 +721,11 @@ class Raise(Statement): """class representing a Raise node""" exc = None if sys.version_info < (3, 0): - _astng_fields = ('exc', 'inst', 'tback') + _astroid_fields = ('exc', 'inst', 'tback') inst = None tback = None else: - _astng_fields = ('exc', 'cause') + _astroid_fields = ('exc', 'cause') exc = None cause = None @@ -721,13 +739,13 @@ def raises_not_implemented(self): class Return(Statement): """class representing a Return node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class Set(NodeNG, Instance, ParentAssignTypeMixin): """class representing a Set node""" - _astng_fields = ('elts',) + _astroid_fields = ('elts',) def __init__(self, elts=None): if elts is None: @@ -744,27 +762,27 @@ def itered(self): class Slice(NodeNG): """class representing a Slice node""" - _astng_fields = ('lower', 'upper', 'step') + _astroid_fields = ('lower', 'upper', 'step') lower = None upper = None step = None class Starred(NodeNG, ParentAssignTypeMixin): """class representing a Starred node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None class Subscript(NodeNG): """class representing a Subscript node""" - _astng_fields = ('value', 'slice') + _astroid_fields = ('value', 'slice') value = None slice = None class TryExcept(BlockRangeMixIn, Statement): """class representing a TryExcept node""" - _astng_fields = ('body', 'handlers', 'orelse',) + _astroid_fields = ('body', 'handlers', 'orelse',) body = None handlers = None orelse = None @@ -790,7 +808,7 @@ def block_range(self, lineno): class TryFinally(BlockRangeMixIn, Statement): """class representing a TryFinally node""" - _astng_fields = ('body', 'finalbody',) + _astroid_fields = ('body', 'finalbody',) body = None finalbody = None @@ -809,7 +827,7 @@ def block_range(self, lineno): class Tuple(NodeNG, Instance, ParentAssignTypeMixin): """class representing a Tuple node""" - _astng_fields = ('elts',) + _astroid_fields = ('elts',) def __init__(self, elts=None): if elts is None: @@ -829,13 +847,13 @@ def itered(self): class UnaryOp(NodeNG): """class representing an UnaryOp node""" - _astng_fields = ('operand',) + _astroid_fields = ('operand',) operand = None class While(BlockRangeMixIn, Statement): """class representing a While node""" - _astng_fields = ('test', 'body', 'orelse',) + _astroid_fields = ('test', 'body', 'orelse',) test = None body = None orelse = None @@ -850,21 +868,24 @@ def block_range(self, lineno): class With(BlockRangeMixIn, AssignTypeMixin, Statement): """class representing a With node""" - _astng_fields = ('expr', 'vars', 'body') - expr = None - vars = None + _astroid_fields = ('items', 'body') + items = None body = None def _blockstart_toline(self): - if self.vars: - return self.vars.tolineno - else: - return self.expr.tolineno + return self.items[-1][0].tolineno + def get_children(self): + for expr, var in self.items: + yield expr + if var: + yield var + for elt in self.body: + yield elt class Yield(NodeNG): """class representing a Yield node""" - _astng_fields = ('value',) + _astroid_fields = ('value',) value = None # constants ############################################################## @@ -889,7 +910,7 @@ def _update_const_classes(): _update_const_classes() def const_factory(value): - """return an astng node for a python value""" + """return an astroid node for a python value""" # XXX we should probably be stricter here and only consider stuff in # CONST_CLS or do better treatment: in case where value is not in CONST_CLS, # we should rather recall the builder on this value than returning an empty diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/nodes.py b/pylibs/pylama/checkers/pylint/astroid/nodes.py similarity index 88% rename from pylibs/pylama/checkers/pylint/logilab/astng/nodes.py rename to pylibs/pylama/checkers/pylint/astroid/nodes.py index b475b902..4deddde3 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/nodes.py +++ b/pylibs/pylama/checkers/pylint/astroid/nodes.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """ on all nodes : .is_statement, returning true if the node should be considered as a @@ -26,7 +26,7 @@ .frame(), returning the first node defining a new local scope (i.e. Module, Function or Class) .set_local(name, node), define an identifier on the first parent frame, - with the node defining it. This is used by the astng builder and should not + with the node defining it. This is used by the astroid builder and should not be used from out there. on From and Import : diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/protocols.py b/pylibs/pylama/checkers/pylint/astroid/protocols.py similarity index 96% rename from pylibs/pylama/checkers/pylint/logilab/astng/protocols.py rename to pylibs/pylama/checkers/pylint/astroid/protocols.py index 05feb246..109d1555 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/protocols.py +++ b/pylibs/pylama/checkers/pylint/astroid/protocols.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """this module contains a set of functions to handle python protocols for nodes where it makes sense. """ @@ -310,10 +310,11 @@ def excepthandler_assigned_stmts(self, node, context=None, asspath=None): def with_assigned_stmts(self, node, context=None, asspath=None): if asspath is None: - for lst in self.vars.infer(context): - if isinstance(lst, (nodes.Tuple, nodes.List)): - for item in lst.nodes: - yield item + for _, vars in self.items: + for lst in vars.infer(context): + if isinstance(lst, (nodes.Tuple, nodes.List)): + for item in lst.nodes: + yield item nodes.With.assigned_stmts = raise_if_nothing_infered(with_assigned_stmts) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py b/pylibs/pylama/checkers/pylint/astroid/raw_building.py similarity index 87% rename from pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py rename to pylibs/pylama/checkers/pylint/astroid/raw_building.py index 076f28f3..b7144a86 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/raw_building.py +++ b/pylibs/pylama/checkers/pylint/astroid/raw_building.py @@ -1,21 +1,21 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . -"""this module contains a set of functions to create astng trees from scratch +# with astroid. If not, see . +"""this module contains a set of functions to create astroid trees from scratch (build_* functions) or from living object (object_build_* functions) """ @@ -30,8 +30,8 @@ from .nodes import (Module, Class, Const, const_factory, From, Function, EmptyNode, Name, Arguments) from .bases import BUILTINS, Generator -from .manager import ASTNGManager -MANAGER = ASTNGManager() +from .manager import AstroidManager +MANAGER = AstroidManager() _CONSTANTS = tuple(CONST_CLS) # the keys of CONST_CLS eg python builtin types @@ -67,14 +67,14 @@ def attach_import_node(node, modname, membername): def build_module(name, doc=None): - """create and initialize a astng Module node""" + """create and initialize a astroid Module node""" node = Module(name, doc, pure_python=False) node.package = False node.parent = None return node def build_class(name, basenames=(), doc=None): - """create and initialize a astng Class node""" + """create and initialize a astroid Class node""" node = Class(name, doc) for base in basenames: basenode = Name() @@ -84,7 +84,7 @@ def build_class(name, basenames=(), doc=None): return node def build_function(name, args=None, defaults=None, flag=0, doc=None): - """create and initialize a astng Function node""" + """create and initialize a astroid Function node""" args, defaults = args or [], defaults or [] # first argument is now a list of decorators func = Function(name, doc) @@ -107,7 +107,7 @@ def build_function(name, args=None, defaults=None, flag=0, doc=None): def build_from_import(fromname, names): - """create and initialize an astng From import statement""" + """create and initialize an astroid From import statement""" return From(fromname, [(name, None) for name in names]) def register_arguments(func, args=None): @@ -129,13 +129,13 @@ def register_arguments(func, args=None): register_arguments(func, arg.elts) def object_build_class(node, member, localname): - """create astng for a living class object""" + """create astroid for a living class object""" basenames = [base.__name__ for base in member.__bases__] return _base_class_object_build(node, member, basenames, localname=localname) def object_build_function(node, member, localname): - """create astng for a living function object""" + """create astroid for a living function object""" args, varargs, varkw, defaults = getargspec(member) if varargs is not None: args.append(varargs) @@ -146,11 +146,11 @@ def object_build_function(node, member, localname): node.add_local_node(func, localname) def object_build_datadescriptor(node, member, name): - """create astng for a living data descriptor object""" + """create astroid for a living data descriptor object""" return _base_class_object_build(node, member, [], name) def object_build_methoddescriptor(node, member, localname): - """create astng for a living method descriptor object""" + """create astroid for a living method descriptor object""" # FIXME get arguments ? func = build_function(getattr(member, '__name__', None) or localname, doc=member.__doc__) @@ -160,7 +160,7 @@ def object_build_methoddescriptor(node, member, localname): node.add_local_node(func, localname) def _base_class_object_build(node, member, basenames, name=None, localname=None): - """create astng for a living class object, with a given set of base names + """create astroid for a living class object, with a given set of base names (e.g. ancestors) """ klass = build_class(name or getattr(member, '__name__', None) or localname, @@ -197,14 +197,14 @@ class InspectBuilder(object): Function and Class nodes and some others as guessed. """ - # astng from living objects ############################################### + # astroid from living objects ############################################### def __init__(self): self._done = {} self._module = None def inspect_build(self, module, modname=None, path=None): - """build astng from a living module (i.e. using inspect) + """build astroid from a living module (i.e. using inspect) this is used when there is no python source code available (either because it's a built-in module or because the .py is not available) """ @@ -217,7 +217,7 @@ def inspect_build(self, module, modname=None, path=None): # in jython, java modules have no __doc__ (see #109562) node = build_module(modname) node.file = node.path = path and abspath(path) or path - MANAGER.astng_cache[modname] = node + MANAGER.astroid_cache[modname] = node node.package = hasattr(module, '__path__') self._done = {} self.object_build(node, module) @@ -289,7 +289,7 @@ def imported_member(self, node, member, name): modname = getattr(member, '__module__', None) except: # XXX use logging - print 'unexpected error while building astng from living object' + print 'unexpected error while building astroid from living object' import traceback traceback.print_exc() modname = None @@ -315,28 +315,28 @@ def imported_member(self, node, member, name): return False -### astng boot strapping ################################################### ### -ASTNG_BUILDER = InspectBuilder() +### astroid boot strapping ################################################### ### +Astroid_BUILDER = InspectBuilder() _CONST_PROXY = {} -def astng_boot_strapping(): - """astng boot strapping the builtins module""" +def astroid_boot_strapping(): + """astroid boot strapping the builtins module""" # this boot strapping is necessary since we need the Const nodes to # inspect_build builtins, and then we can proxy Const - from ..common.compat import builtins - astng_builtin = ASTNG_BUILDER.inspect_build(builtins) + from ..logilab.common.compat import builtins + astroid_builtin = Astroid_BUILDER.inspect_build(builtins) for cls, node_cls in CONST_CLS.items(): if cls is type(None): proxy = build_class('NoneType') - proxy.parent = astng_builtin + proxy.parent = astroid_builtin else: - proxy = astng_builtin.getattr(cls.__name__)[0] + proxy = astroid_builtin.getattr(cls.__name__)[0] if cls in (dict, list, set, tuple): node_cls._proxied = proxy else: _CONST_PROXY[cls] = proxy -astng_boot_strapping() +astroid_boot_strapping() # TODO : find a nicer way to handle this situation; # However __proxied introduced an @@ -347,5 +347,5 @@ def _set_proxied(const): from types import GeneratorType Generator._proxied = Class(GeneratorType.__name__, GeneratorType.__doc__) -ASTNG_BUILDER.object_build(Generator._proxied, GeneratorType) +Astroid_BUILDER.object_build(Generator._proxied, GeneratorType) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py b/pylibs/pylama/checkers/pylint/astroid/rebuilder.py similarity index 90% rename from pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py rename to pylibs/pylama/checkers/pylint/astroid/rebuilder.py index 03ee6128..d0f6bde2 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/rebuilder.py +++ b/pylibs/pylama/checkers/pylint/astroid/rebuilder.py @@ -1,25 +1,26 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """this module contains utilities for rebuilding a _ast tree in -order to get a single ASTNG representation +order to get a single Astroid representation """ import sys +from warnings import warn from _ast import (Expr as Discard, Str, # binary operators Add, Div, FloorDiv, Mod, Mult, Pow, Sub, BitAnd, BitOr, BitXor, @@ -117,29 +118,56 @@ def _set_infos(oldnode, newnode, parent): class TreeRebuilder(object): - """Rebuilds the _ast tree to become an ASTNG tree""" + """Rebuilds the _ast tree to become an Astroid tree""" - _visit_meths = {} - def __init__(self): - self.init() - - def init(self): + def __init__(self, manager): + self._manager = manager self.asscontext = None self._metaclass = [''] self._global_names = [] self._from_nodes = [] self._delayed_assattr = [] + self._visit_meths = {} + + def _transform(self, node): + try: + transforms = self._manager.transforms[type(node)] + except KeyError: + return node # no transform registered for this class of node + orig_node = node # copy the reference + for transform_func, predicate in transforms: + if predicate is None or predicate(node): + ret = transform_func(node) + # if the transformation function returns something, it's + # expected to be a replacement for the node + if ret is not None: + if node is not orig_node: + # node has already be modified by some previous + # transformation, warn about it + warn('node %s substitued multiple times' % node) + node = ret + return node + + def visit_module(self, node, modname, package): + """visit a Module node by returning a fresh instance of it""" + newnode = new.Module(modname, None) + newnode.package = package + _lineno_parent(node, newnode, parent=None) + _init_set_doc(node, newnode) + newnode.body = [self.visit(child, newnode) for child in node.body] + newnode.set_line_info(newnode.last_child()) + return self._transform(newnode) def visit(self, node, parent): cls = node.__class__ if cls in self._visit_meths: - return self._visit_meths[cls](node, parent) + visit_method = self._visit_meths[cls] else: cls_name = cls.__name__ visit_name = 'visit_' + REDIRECT.get(cls_name, cls_name).lower() visit_method = getattr(self, visit_name) self._visit_meths[cls] = visit_method - return visit_method(node, parent) + return self._transform(visit_method(node, parent)) def _save_assignment(self, node, name=None): """save assignement situation since node.parent is not available yet""" @@ -157,6 +185,8 @@ def visit_arguments(self, node, parent): newnode.args = [self.visit(child, newnode) for child in node.args] self.asscontext = None newnode.defaults = [self.visit(child, newnode) for child in node.defaults] + newnode.kwonlyargs = [] + newnode.kw_defaults = [] newnode.vararg = node.vararg newnode.kwarg = node.kwarg # save argument names in locals: @@ -287,7 +317,7 @@ def visit_callfunc(self, node, parent): return newnode def visit_class(self, node, parent): - """visit a Class node to become astng""" + """visit a Class node to become astroid""" self._metaclass.append(self._metaclass[-1]) newnode = new.Class(node.name, None) _lineno_parent(node, newnode, parent) @@ -342,7 +372,7 @@ def visit_comprehension(self, node, parent): def visit_decorators(self, node, parent): """visit a Decorators node by returning a fresh instance of it""" # /!\ node is actually a _ast.Function node while - # parent is a astng.nodes.Function node + # parent is a astroid.nodes.Function node newnode = new.Decorators() _lineno_parent(node, newnode, parent) if 'decorators' in node._fields: # py < 2.6, i.e. 2.5 @@ -461,7 +491,7 @@ def visit_from(self, node, parent): return newnode def visit_function(self, node, parent): - """visit an Function node to become astng""" + """visit an Function node to become astroid""" self._global_names.append({}) newnode = new.Function(node.name, None) _lineno_parent(node, newnode, parent) @@ -523,7 +553,7 @@ def visit_getattr(self, node, parent): return newnode def visit_global(self, node, parent): - """visit an Global node to become astng""" + """visit an Global node to become astroid""" newnode = new.Global(node.names) _set_infos(node, newnode, parent) if self._global_names: # global at the module level, no effect @@ -606,16 +636,6 @@ def visit_listcomp(self, node, parent): newnode.set_line_info(newnode.last_child()) return newnode - def visit_module(self, node, modname, package): - """visit a Module node by returning a fresh instance of it""" - newnode = new.Module(modname, None) - newnode.package = package - _lineno_parent(node, newnode, parent=None) - _init_set_doc(node, newnode) - newnode.body = [self.visit(child, newnode) for child in node.body] - newnode.set_line_info(newnode.last_child()) - return newnode - def visit_name(self, node, parent): """visit a Name node by returning a fresh instance of it""" # True and False can be assigned to something in py2x, so we have to @@ -787,12 +807,14 @@ def visit_while(self, node, parent): def visit_with(self, node, parent): newnode = new.With() _lineno_parent(node, newnode, parent) - _node = getattr(node, 'items', [node])[0] # python 3.3 XXX - newnode.expr = self.visit(_node.context_expr, newnode) + expr = self.visit(node.context_expr, newnode) self.asscontext = "Ass" - if _node.optional_vars is not None: - newnode.vars = self.visit(_node.optional_vars, newnode) + if node.optional_vars is not None: + vars = self.visit(node.optional_vars, newnode) + else: + vars = None self.asscontext = None + newnode.items = [(expr, vars)] newnode.body = [self.visit(child, newnode) for child in node.body] newnode.set_line_info(newnode.last_child()) return newnode @@ -813,9 +835,17 @@ class TreeRebuilder3k(TreeRebuilder): def visit_arg(self, node, parent): """visit a arg node by returning a fresh AssName instance""" # the node is coming from py>=3.0, but we use AssName in py2.x - # XXX or we should instead introduce a Arg node in astng ? + # XXX or we should instead introduce a Arg node in astroid ? return self.visit_assname(node, parent, node.arg) + def visit_arguments(self, node, parent): + newnode = super(TreeRebuilder3k, self).visit_arguments(node, parent) + self.asscontext = "Ass" + newnode.kwonlyargs = [self.visit(child, newnode) for child in node.kwonlyargs] + self.asscontext = None + newnode.kw_defaults = [self.visit(child, newnode) if child else None for child in node.kw_defaults] + return newnode + def visit_excepthandler(self, node, parent): """visit an ExceptHandler node by returning a fresh instance of it""" newnode = new.ExceptHandler() @@ -862,10 +892,11 @@ def visit_try(self, node, parent): newnode.finalbody = [self.visit(n, newnode) for n in node.finalbody] if node.handlers: excnode = new.TryExcept() - _lineno_parent(node, excnode, parent) - excnode.body = [self.visit(child, newnode) for child in node.body] - excnode.handlers = [self.visit(child, newnode) for child in node.handlers] - excnode.orelse = [self.visit(child, newnode) for child in node.orelse] + _lineno_parent(node, excnode, newnode) + excnode.body = [self.visit(child, excnode) for child in node.body] + excnode.handlers = [self.visit(child, excnode) for child in node.handlers] + excnode.orelse = [self.visit(child, excnode) for child in node.orelse] + excnode.set_line_info(excnode.last_child()) newnode.body = [excnode] else: newnode.body = [self.visit(child, newnode) for child in node.body] @@ -878,6 +909,28 @@ def visit_try(self, node, parent): newnode.set_line_info(newnode.last_child()) return newnode + def visit_with(self, node, parent): + if 'items' not in node._fields: + # python < 3.3 + return super(TreeRebuilder3k, self).visit_with(node, parent) + + newnode = new.With() + _lineno_parent(node, newnode, parent) + def visit_child(child): + expr = self.visit(child.context_expr, newnode) + self.asscontext = 'Ass' + if child.optional_vars: + var = self.visit(child.optional_vars, newnode) + else: + var = None + self.asscontext = None + return expr, var + newnode.items = [visit_child(child) + for child in node.items] + newnode.body = [self.visit(child, newnode) for child in node.body] + newnode.set_line_info(newnode.last_child()) + return newnode + def visit_yieldfrom(self, node, parent): return self.visit_yield(node, parent) diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py b/pylibs/pylama/checkers/pylint/astroid/scoped_nodes.py similarity index 91% rename from pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py rename to pylibs/pylama/checkers/pylint/astroid/scoped_nodes.py index 8e049775..e52c89f7 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/scoped_nodes.py +++ b/pylibs/pylama/checkers/pylint/astroid/scoped_nodes.py @@ -1,20 +1,20 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """This module contains the classes for "scoped" node, i.e. which are opening a new local scope in the language definition : Module, Class, Function (and Lambda, GenExpr, DictComp and SetComp to some extent). @@ -26,11 +26,11 @@ import sys from itertools import chain -from ..common.compat import builtins -from ..common.decorators import cached +from ..logilab.common.compat import builtins +from ..logilab.common.decorators import cached from .exceptions import NotFoundError, \ - ASTNGBuildingException, InferenceError + AstroidBuildingException, InferenceError from .node_classes import Const, DelName, DelAttr, \ Dict, From, List, Pass, Raise, Return, Tuple, Yield, \ LookupMixIn, const_factory as cf, unpack_infer @@ -39,7 +39,7 @@ BUILTINS from .mixins import FilterStmtsMixin from .bases import Statement -from .manager import ASTNGManager +from .manager import AstroidManager def remove_nodes(func, cls): @@ -72,20 +72,20 @@ def std_special_attributes(self, name, add_locals=True): return [Dict()] + locals.get(name, []) raise NotFoundError(name) -MANAGER = ASTNGManager() +MANAGER = AstroidManager() def builtin_lookup(name): """lookup a name into the builtin module - return the list of matching statements and the astng for the builtin + return the list of matching statements and the astroid for the builtin module """ - builtin_astng = MANAGER.astng_from_module(builtins) + builtin_astroid = MANAGER.ast_from_module(builtins) if name == '__dict__': - return builtin_astng, () + return builtin_astroid, () try: - stmts = builtin_astng.locals[name] + stmts = builtin_astroid.locals[name] except KeyError: stmts = () - return builtin_astng, stmts + return builtin_astroid, stmts # TODO move this Mixin to mixins.py; problem: 'Function' in _scope_lookup @@ -207,14 +207,14 @@ def __contains__(self, name): # Module ##################################################################### class Module(LocalsDictNodeNG): - _astng_fields = ('body',) + _astroid_fields = ('body',) fromlineno = 0 lineno = 0 # attributes below are set by the builder module or by raw factories - # the file from which as been extracted the astng representation. It may + # the file from which as been extracted the astroid representation. It may # be None if the representation has been built from a built-in module file = None # encoding of python source file, so we can get unicode out of it (python2 @@ -222,7 +222,7 @@ class Module(LocalsDictNodeNG): file_encoding = None # the module name name = None - # boolean for astng built from source (i.e. ast) + # boolean for astroid built from source (i.e. ast) pure_python = None # boolean for package module package = None @@ -246,7 +246,7 @@ def __init__(self, name, doc, pure_python=True): @property def file_stream(self): if self.file is not None: - return open(self.file) + return open(self.file, 'rb') return None def block_range(self, lineno): @@ -282,7 +282,7 @@ def getattr(self, name, context=None, ignore_locals=False): if self.package: try: return [self.import_module(name, relative_only=True)] - except ASTNGBuildingException: + except AstroidBuildingException: raise NotFoundError(name) except Exception:# XXX pylint tests never pass here; do we need it? import traceback @@ -336,13 +336,13 @@ def import_module(self, modname, relative_only=False, level=None): level = 0 absmodname = self.relative_to_absolute_name(modname, level) try: - return MANAGER.astng_from_module_name(absmodname) - except ASTNGBuildingException: + return MANAGER.ast_from_module_name(absmodname) + except AstroidBuildingException: # we only want to import a sub module or package of this module, # skip here if relative_only: raise - return MANAGER.astng_from_module_name(modname) + return MANAGER.ast_from_module_name(modname) def relative_to_absolute_name(self, modname, level): """return the absolute module name for a relative import. @@ -350,7 +350,7 @@ def relative_to_absolute_name(self, modname, level): The relative import can be implicit or explicit. """ # XXX this returns non sens when called on an absolute import - # like 'pylint.checkers.logilab.astng.utils' + # like 'pylint.checkers.astroid.utils' # XXX doesn't return absolute name if self.name isn't absolute name if self.absolute_import_activated() and level is None: return modname @@ -387,7 +387,7 @@ def wildcard_import_names(self): except AttributeError: return [name for name in living.__dict__.keys() if not name.startswith('_')] - # else lookup the astng + # else lookup the astroid # # We separate the different steps of lookup in try/excepts # to avoid catching too many Exceptions @@ -419,7 +419,7 @@ def frame(self): class GenExpr(ComprehensionScope): - _astng_fields = ('elt', 'generators') + _astroid_fields = ('elt', 'generators') def __init__(self): self.locals = {} @@ -428,7 +428,7 @@ def __init__(self): class DictComp(ComprehensionScope): - _astng_fields = ('key', 'value', 'generators') + _astroid_fields = ('key', 'value', 'generators') def __init__(self): self.locals = {} @@ -438,7 +438,7 @@ def __init__(self): class SetComp(ComprehensionScope): - _astng_fields = ('elt', 'generators') + _astroid_fields = ('elt', 'generators') def __init__(self): self.locals = {} @@ -448,7 +448,7 @@ def __init__(self): class _ListComp(NodeNG): """class representing a ListComp node""" - _astng_fields = ('elt', 'generators') + _astroid_fields = ('elt', 'generators') elt = None generators = None @@ -465,7 +465,7 @@ class ListComp(_ListComp): class Lambda(LocalsDictNodeNG, FilterStmtsMixin): - _astng_fields = ('args', 'body',) + _astroid_fields = ('args', 'body',) name = '' # function's type, 'function' | 'method' | 'staticmethod' | 'classmethod' @@ -506,7 +506,7 @@ def infer_call_result(self, caller, context=None): return self.body.infer(context) def scope_lookup(self, node, name, offset=0): - if node in self.args.defaults: + if node in self.args.defaults or node in self.args.kw_defaults: frame = self.parent.frame() # line offset to avoid that def func(f=func) resolve the default # value to the defined function @@ -518,7 +518,7 @@ def scope_lookup(self, node, name, offset=0): class Function(Statement, Lambda): - _astng_fields = ('decorators', 'args', 'body') + _astroid_fields = ('decorators', 'args', 'body') special_attributes = set(('__name__', '__doc__', '__dict__')) is_function = True @@ -542,6 +542,8 @@ def set_line_info(self, lastchild): if self.decorators is not None: self.fromlineno += sum(node.tolineno - node.lineno + 1 for node in self.decorators.nodes) + if self.args.fromlineno < self.fromlineno: + self.args.fromlineno = self.fromlineno self.tolineno = lastchild.tolineno self.blockstart_tolineno = self.args.tolineno @@ -586,10 +588,23 @@ def is_bound(self): return self.type == 'classmethod' def is_abstract(self, pass_is_abstract=True): - """return true if the method is abstract - It's considered as abstract if the only statement is a raise of - NotImplementError, or, if pass_is_abstract, a pass statement + """Returns True if the method is abstract. + + A method is considered abstract if + - the only statement is 'raise NotImplementedError', or + - the only statement is 'pass' and pass_is_abstract is True, or + - the method is annotated with abc.astractproperty/abc.abstractmethod """ + if self.decorators: + for node in self.decorators.nodes: + try: + infered = node.infer().next() + except InferenceError: + continue + if infered and infered.qname() in ('abc.abstractproperty', + 'abc.abstractmethod'): + return True + for child_node in self.body: if isinstance(child_node, Raise): if child_node.raises_not_implemented(): @@ -683,7 +698,7 @@ class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin): # by a raw factories # a dictionary of class instances attributes - _astng_fields = ('decorators', 'bases', 'body') # name + _astroid_fields = ('decorators', 'bases', 'body') # name decorators = None special_attributes = set(('__name__', '__doc__', '__dict__', '__module__', @@ -802,20 +817,20 @@ def ancestors(self, recurs=True, context=None): continue def local_attr_ancestors(self, name, context=None): - """return an iterator on astng representation of parent classes + """return an iterator on astroid representation of parent classes which have defined in their locals """ - for astng in self.ancestors(context=context): - if name in astng: - yield astng + for astroid in self.ancestors(context=context): + if name in astroid: + yield astroid def instance_attr_ancestors(self, name, context=None): - """return an iterator on astng representation of parent classes + """return an iterator on astroid representation of parent classes which have defined in their instance attribute dictionary """ - for astng in self.ancestors(context=context): - if name in astng.instance_attrs: - yield astng + for astroid in self.ancestors(context=context): + if name in astroid.instance_attrs: + yield astroid def has_base(self, node): return node in self.bases @@ -838,7 +853,7 @@ def local_attr(self, name, context=None): local_attr = remove_nodes(local_attr, DelAttr) def instance_attr(self, name, context=None): - """return the astng nodes associated to name in this class instance + """return the astroid nodes associated to name in this class instance attributes dictionary and in its parents :raises `NotFoundError`: @@ -940,8 +955,8 @@ def methods(self): its ancestors """ done = {} - for astng in chain(iter((self,)), self.ancestors()): - for meth in astng.mymethods(): + for astroid in chain(iter((self,)), self.ancestors()): + for meth in astroid.mymethods(): if meth.name in done: continue done[meth.name] = None diff --git a/pylibs/pylama/checkers/pylint/logilab/astng/utils.py b/pylibs/pylama/checkers/pylint/astroid/utils.py similarity index 90% rename from pylibs/pylama/checkers/pylint/logilab/astng/utils.py rename to pylibs/pylama/checkers/pylint/astroid/utils.py index 325535b0..322e25b2 100644 --- a/pylibs/pylama/checkers/pylint/logilab/astng/utils.py +++ b/pylibs/pylama/checkers/pylint/astroid/utils.py @@ -1,27 +1,27 @@ # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr # -# This file is part of logilab-astng. +# This file is part of astroid. # -# logilab-astng is free software: you can redistribute it and/or modify it +# astroid is free software: you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by the # Free Software Foundation, either version 2.1 of the License, or (at your # option) any later version. # -# logilab-astng is distributed in the hope that it will be useful, but +# astroid is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License # for more details. # # You should have received a copy of the GNU Lesser General Public License along -# with logilab-astng. If not, see . +# with astroid. If not, see . """this module contains some utilities to navigate in the tree or to extract information from it """ __docformat__ = "restructuredtext en" -from .exceptions import ASTNGBuildingException +from .exceptions import AstroidBuildingException from .builder import parse @@ -123,12 +123,12 @@ def _check_children(node): if not ok: print "lines;", node.lineno, child.lineno print "of module", node.root(), node.root().name - raise ASTNGBuildingException + raise AstroidBuildingException _check_children(child) class TreeTester(object): - '''A helper class to see _ast tree and compare with astng tree + '''A helper class to see _ast tree and compare with astroid tree indent: string for tree indent representation lineno: bool to tell if we should print the line numbers @@ -141,7 +141,7 @@ class TreeTester(object): . . . nl = True . ] - >>> print tester.astng_tree_repr() + >>> print tester.astroid_tree_repr() Module() body = [ Print() @@ -219,16 +219,16 @@ def _native_repr_tree(self, node, indent, _done=None): self._string += '\n' + indent + field + " = " + repr(attr) - def build_astng_tree(self): - """build astng tree from the _ast tree + def build_astroid_tree(self): + """build astroid tree from the _ast tree """ - from logilab.astng.builder import ASTNGBuilder - tree = ASTNGBuilder().string_build(self.sourcecode) + from .builder import AstroidBuilder + tree = AstroidBuilder().string_build(self.sourcecode) return tree - def astng_tree_repr(self, ids=False): - """build the astng tree and return a nice tree representation""" - mod = self.build_astng_tree() + def astroid_tree_repr(self, ids=False): + """build the astroid tree and return a nice tree representation""" + mod = self.build_astroid_tree() return mod.repr_tree(ids) diff --git a/pylibs/pylama/checkers/pylint/checkers/__init__.py b/pylibs/pylama/checkers/pylint/checkers/__init__.py index bc722279..dddc1bab 100644 --- a/pylibs/pylama/checkers/pylint/checkers/__init__.py +++ b/pylibs/pylama/checkers/pylint/checkers/__init__.py @@ -38,15 +38,16 @@ """ +import sys import tokenize -from os import listdir -from os.path import dirname, join, isdir, splitext +import warnings +from os.path import dirname -from ..logilab.astng.utils import ASTWalker -from ..logilab.common.modutils import load_module_from_file +from ..astroid.utils import ASTWalker from ..logilab.common.configuration import OptionsProviderMixIn -from ..reporters import diff_string, EmptyReport +from ..reporters import diff_string +from ..utils import register_plugins def table_lines_from_stats(stats, old_stats, columns): """get values listed in from and , @@ -121,43 +122,31 @@ def process_module(self, node): stream must implement the readline method """ + warnings.warn("Modules that need access to the tokens should " + "use the ITokenChecker interface.", + DeprecationWarning) stream = node.file_stream - stream.seek(0) # XXX may be removed with astng > 0.23 - self.process_tokens(tokenize.generate_tokens(stream.readline)) + stream.seek(0) # XXX may be removed with astroid > 0.23 + if sys.version_info <= (3, 0): + self.process_tokens(tokenize.generate_tokens(stream.readline)) + else: + self.process_tokens(tokenize.tokenize(stream.readline)) def process_tokens(self, tokens): """should be overridden by subclasses""" raise NotImplementedError() -PY_EXTS = ('.py', '.pyc', '.pyo', '.pyw', '.so', '.dll') +class BaseTokenChecker(BaseChecker): + """Base class for checkers that want to have access to the token stream.""" + + def process_tokens(self, tokens): + """Should be overridden by subclasses.""" + raise NotImplementedError() + def initialize(linter): """initialize linter with checkers in this package """ - package_load(linter, __path__[0]) + register_plugins(linter, __path__[0]) -def package_load(linter, directory): - """load all module and package in the given directory, looking for a - 'register' function in each one, used to register pylint checkers - """ - imported = {} - for filename in listdir(directory): - basename, extension = splitext(filename) - if basename in imported or basename == '__pycache__': - continue - if extension in PY_EXTS and basename != '__init__' or ( - not extension and isdir(join(directory, basename))): - try: - module = load_module_from_file(join(directory, filename)) - except ValueError: - # empty module name (usually emacs auto-save files) - continue - except ImportError, exc: - import sys - print >> sys.stderr, "Problem importing module %s: %s" % (filename, exc) - else: - if hasattr(module, 'register'): - module.register(linter) - imported[basename] = 1 - -__all__ = ('BaseChecker', 'initialize', 'package_load') +__all__ = ('BaseChecker', 'initialize') diff --git a/pylibs/pylama/checkers/pylint/checkers/base.py b/pylibs/pylama/checkers/pylint/checkers/base.py index 495076f8..5282898e 100644 --- a/pylibs/pylama/checkers/pylint/checkers/base.py +++ b/pylibs/pylama/checkers/pylint/checkers/base.py @@ -1,6 +1,7 @@ # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). -# Copyright (c) 2009-2010 Arista Networks, Inc. # http://www.logilab.fr/ -- mailto:contact@logilab.fr +# Copyright (c) 2009-2010 Arista Networks, Inc. +# # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -15,30 +16,35 @@ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """basic checker for Python code""" - -from ..logilab import astng +import sys +from .. import astroid from ..logilab.common.ureports import Table -from ..logilab.astng import are_exclusive +from ..astroid import are_exclusive, bases -from ..interfaces import IASTNGChecker +from ..interfaces import IAstroidChecker +from ..utils import EmptyReport from ..reporters import diff_string -from ..checkers import BaseChecker, EmptyReport -from ..checkers.utils import ( +from . import BaseChecker +from .utils import ( check_messages, clobber_in_except, + is_builtin_object, is_inside_except, + overrides_a_method, safe_infer, ) import re + # regex for class/function/variable/constant name CLASS_NAME_RGX = re.compile('[A-Z_][a-zA-Z0-9]+$') MOD_NAME_RGX = re.compile('(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$') CONST_NAME_RGX = re.compile('(([A-Z_][A-Z0-9_]*)|(__.*__))$') COMP_VAR_RGX = re.compile('[A-Za-z_][A-Za-z0-9_]*$') DEFAULT_NAME_RGX = re.compile('[a-z_][a-z0-9_]{2,30}$') +CLASS_ATTRIBUTE_RGX = re.compile(r'([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$') # do not require a doc string on system methods NO_REQUIRED_DOC_RGX = re.compile('__.*__') @@ -48,8 +54,8 @@ def in_loop(node): """return True if the node is inside a kind of for loop""" parent = node.parent while parent is not None: - if isinstance(parent, (astng.For, astng.ListComp, astng.SetComp, - astng.DictComp, astng.GenExpr)): + if isinstance(parent, (astroid.For, astroid.ListComp, astroid.SetComp, + astroid.DictComp, astroid.GenExpr)): return True parent = parent.parent return False @@ -68,17 +74,48 @@ def in_nested_list(nested_list, obj): def _loop_exits_early(loop): """Returns true if a loop has a break statement in its body.""" - loop_nodes = (astng.For, astng.While) + loop_nodes = (astroid.For, astroid.While) # Loop over body explicitly to avoid matching break statements # in orelse. for child in loop.body: if isinstance(child, loop_nodes): continue - for _ in child.nodes_of_class(astng.Break, skip_klass=loop_nodes): + for _ in child.nodes_of_class(astroid.Break, skip_klass=loop_nodes): return True return False + +def _determine_function_name_type(node): + """Determine the name type whose regex the a function's name should match. + + :param node: A function node. + :returns: One of ('function', 'method', 'attr') + """ + if not node.is_method(): + return 'function' + if node.decorators: + decorators = node.decorators.nodes + else: + decorators = [] + for decorator in decorators: + # If the function is a property (decorated with @property + # or @abc.abstractproperty), the name type is 'attr'. + if (isinstance(decorator, astroid.Name) or + (isinstance(decorator, astroid.Getattr) and + decorator.attrname == 'abstractproperty')): + infered = safe_infer(decorator) + if (infered and + infered.qname() in ('__builtin__.property', 'abc.abstractproperty')): + return 'attr' + # If the function is decorated using the prop_method.{setter,getter} + # form, treat it like an attribute as well. + elif (isinstance(decorator, astroid.Getattr) and + decorator.attrname in ('setter', 'deleter')): + return 'attr' + return 'method' + + def report_by_type_stats(sect, stats, old_stats): """make a report of @@ -130,13 +167,13 @@ def x(self, value): self._x = value """ if node.decorators: for decorator in node.decorators.nodes: - if (isinstance(decorator, astng.Getattr) and + if (isinstance(decorator, astroid.Getattr) and getattr(decorator.expr, 'name', None) == node.name): return True return False class _BasicChecker(BaseChecker): - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'basic' class BasicErrorChecker(_BasicChecker): @@ -187,79 +224,85 @@ class BasicErrorChecker(_BasicChecker): def __init__(self, linter): _BasicChecker.__init__(self, linter) - @check_messages('E0102') + @check_messages('function-redefined') def visit_class(self, node): self._check_redefinition('class', node) - @check_messages('E0100', 'E0101', 'E0102', 'E0106', 'E0108') + @check_messages('init-is-generator', 'return-in-init', + 'function-redefined', 'return-arg-in-generator', + 'duplicate-argument-name') def visit_function(self, node): if not redefined_by_decorator(node): self._check_redefinition(node.is_method() and 'method' or 'function', node) # checks for max returns, branch, return in __init__ - returns = node.nodes_of_class(astng.Return, - skip_klass=(astng.Function, astng.Class)) + returns = node.nodes_of_class(astroid.Return, + skip_klass=(astroid.Function, astroid.Class)) if node.is_method() and node.name == '__init__': if node.is_generator(): - self.add_message('E0100', node=node) + self.add_message('init-is-generator', node=node) else: values = [r.value for r in returns] - if [v for v in values if not (v is None or - (isinstance(v, astng.Const) and v.value is None) - or (isinstance(v, astng.Name) and v.name == 'None'))]: - self.add_message('E0101', node=node) + # Are we returning anything but None from constructors + if [v for v in values if + not (v is None or + (isinstance(v, astroid.Const) and v.value is None) or + (isinstance(v, astroid.Name) and v.name == 'None') + ) ]: + self.add_message('return-in-init', node=node) elif node.is_generator(): # make sure we don't mix non-None returns and yields for retnode in returns: - if isinstance(retnode.value, astng.Const) and \ + if isinstance(retnode.value, astroid.Const) and \ retnode.value.value is not None: - self.add_message('E0106', node=node, + self.add_message('return-arg-in-generator', node=node, line=retnode.fromlineno) + # Check for duplicate names args = set() for name in node.argnames(): if name in args: - self.add_message('E0108', node=node, args=(name,)) + self.add_message('duplicate-argument-name', node=node, args=(name,)) else: args.add(name) - @check_messages('E0104') + @check_messages('return-outside-function') def visit_return(self, node): - if not isinstance(node.frame(), astng.Function): - self.add_message('E0104', node=node) + if not isinstance(node.frame(), astroid.Function): + self.add_message('return-outside-function', node=node) - @check_messages('E0105') + @check_messages('yield-outside-function') def visit_yield(self, node): - if not isinstance(node.frame(), (astng.Function, astng.Lambda)): - self.add_message('E0105', node=node) + if not isinstance(node.frame(), (astroid.Function, astroid.Lambda)): + self.add_message('yield-outside-function', node=node) - @check_messages('E0103') + @check_messages('not-in-loop') def visit_continue(self, node): self._check_in_loop(node, 'continue') - @check_messages('E0103') + @check_messages('not-in-loop') def visit_break(self, node): self._check_in_loop(node, 'break') - @check_messages('W0120') + @check_messages('useless-else-on-loop') def visit_for(self, node): self._check_else_on_loop(node) - @check_messages('W0120') + @check_messages('useless-else-on-loop') def visit_while(self, node): self._check_else_on_loop(node) - @check_messages('E0107') + @check_messages('nonexistent-operator') def visit_unaryop(self, node): - """check use of the non-existent ++ adn -- operator operator""" + """check use of the non-existent ++ and -- operator operator""" if ((node.op in '+-') and - isinstance(node.operand, astng.UnaryOp) and + isinstance(node.operand, astroid.UnaryOp) and (node.operand.op == node.op)): - self.add_message('E0107', node=node, args=node.op*2) + self.add_message('nonexistent-operator', node=node, args=node.op*2) def _check_else_on_loop(self, node): """Check that any loop with an else clause has a break statement.""" if node.orelse and not _loop_exits_early(node): - self.add_message('W0120', node=node, + self.add_message('useless-else-on-loop', node=node, # This is not optimal, but the line previous # to the first statement in the else clause # will usually be the one that contains the else:. @@ -269,17 +312,17 @@ def _check_in_loop(self, node, node_name): """check that a node is inside a for or while loop""" _node = node.parent while _node: - if isinstance(_node, (astng.For, astng.While)): + if isinstance(_node, (astroid.For, astroid.While)): break _node = _node.parent else: - self.add_message('E0103', node=node, args=node_name) + self.add_message('not-in-loop', node=node, args=node_name) def _check_redefinition(self, redeftype, node): """check for redefinition of a function / method / class name""" defined_self = node.parent.frame()[node.name] if defined_self is not node and not are_exclusive(node, defined_self): - self.add_message('E0102', node=node, + self.add_message('function-redefined', node=node, args=(redeftype, defined_self.fromlineno)) @@ -287,7 +330,6 @@ def _check_redefinition(self, redeftype, node): class BasicChecker(_BasicChecker): """checks for : * doc strings - * modules / classes / functions / methods / arguments / variables name * number of arguments, local variables, branches, returns and statements in functions, methods * required module attributes @@ -296,7 +338,7 @@ class BasicChecker(_BasicChecker): * uses of the global statement """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'basic' msgs = { @@ -332,11 +374,10 @@ class BasicChecker(_BasicChecker): 'duplicate-key', "Used when a dictionary expression binds the same key multiple \ times."), - 'W0122': ('Use of the exec statement', - 'exec-statement', - 'Used when you use the "exec" statement, to discourage its \ + 'W0122': ('Use of exec', + 'exec-used', + 'Used when you use the "exec" statement (function for Python 3), to discourage its \ usage. That doesn\'t mean you can not use it !'), - 'W0141': ('Used builtin function %r', 'bad-builtin', 'Used when a black listed builtin function is used (see the ' @@ -359,11 +400,15 @@ class BasicChecker(_BasicChecker): 'A call of assert on a tuple will always evaluate to true if ' 'the tuple is not empty, and will always evaluate to false if ' 'it is.'), + 'W0121': ('Use raise ErrorClass(args) instead of raise ErrorClass, args.', + 'old-raise-syntax', + "Used when the alternate raise syntax 'raise foo, bar' is used " + "instead of 'raise foo(bar)'.", + {'maxversion': (3, 0)}), 'C0121': ('Missing required attribute "%s"', # W0103 'missing-module-attribute', 'Used when an attribute required for modules is missing.'), - } options = (('required-attributes', @@ -392,14 +437,14 @@ def open(self): self._tryfinallys = [] self.stats = self.linter.add_stats(module=0, function=0, method=0, class_=0) - + @check_messages('missing-module-attribute') def visit_module(self, node): """check module name, docstring and required arguments """ self.stats['module'] += 1 for attr in self.config.required_attributes: if attr not in node: - self.add_message('C0121', node=node, args=attr) + self.add_message('missing-module-attribute', node=node, args=attr) def visit_class(self, node): """check module name, docstring and redefinition @@ -407,31 +452,32 @@ def visit_class(self, node): """ self.stats['class'] += 1 - @check_messages('W0104', 'W0105') + @check_messages('pointless-statement', 'pointless-string-statement', + 'expression-not-assigned') def visit_discard(self, node): """check for various kind of statements without effect""" expr = node.value - if isinstance(expr, astng.Const) and isinstance(expr.value, + if isinstance(expr, astroid.Const) and isinstance(expr.value, basestring): # treat string statement in a separated message - self.add_message('W0105', node=node) + self.add_message('pointless-string-statement', node=node) return # ignore if this is : # * a direct function call # * the unique child of a try/except body # * a yield (which are wrapped by a discard node in _ast XXX) # warn W0106 if we have any underlying function call (we can't predict - # side effects), else W0104 - if (isinstance(expr, (astng.Yield, astng.CallFunc)) or - (isinstance(node.parent, astng.TryExcept) and + # side effects), else pointless-statement + if (isinstance(expr, (astroid.Yield, astroid.CallFunc)) or + (isinstance(node.parent, astroid.TryExcept) and node.parent.body == [node])): return - if any(expr.nodes_of_class(astng.CallFunc)): - self.add_message('W0106', node=node, args=expr.as_string()) + if any(expr.nodes_of_class(astroid.CallFunc)): + self.add_message('expression-not-assigned', node=node, args=expr.as_string()) else: - self.add_message('W0104', node=node) + self.add_message('pointless-statement', node=node) - @check_messages('W0108') + @check_messages('unnecessary-lambda') def visit_lambda(self, node): """check whether or not the lambda is suspicious """ @@ -446,11 +492,11 @@ def visit_lambda(self, node): # of the lambda. return call = node.body - if not isinstance(call, astng.CallFunc): + if not isinstance(call, astroid.CallFunc): # The body of the lambda must be a function call expression # for the lambda to be unnecessary. return - # XXX are lambda still different with astng >= 0.18 ? + # XXX are lambda still different with astroid >= 0.18 ? # *args and **kwargs need to be treated specially, since they # are structured differently between the lambda and the function # call (in the lambda they appear in the args.args list and are @@ -460,14 +506,14 @@ def visit_lambda(self, node): ordinary_args = list(node.args.args) if node.args.kwarg: if (not call.kwargs - or not isinstance(call.kwargs, astng.Name) + or not isinstance(call.kwargs, astroid.Name) or node.args.kwarg != call.kwargs.name): return elif call.kwargs: return if node.args.vararg: if (not call.starargs - or not isinstance(call.starargs, astng.Name) + or not isinstance(call.starargs, astroid.Name) or node.args.vararg != call.starargs.name): return elif call.starargs: @@ -477,12 +523,13 @@ def visit_lambda(self, node): if len(ordinary_args) != len(call.args): return for i in xrange(len(ordinary_args)): - if not isinstance(call.args[i], astng.Name): + if not isinstance(call.args[i], astroid.Name): return if node.args.args[i].name != call.args[i].name: return - self.add_message('W0108', line=node.fromlineno, node=node) + self.add_message('unnecessary-lambda', line=node.fromlineno, node=node) + @check_messages('dangerous-default-value') def visit_function(self, node): """check function name, docstring, arguments, redefinition, variable names, max locals @@ -492,19 +539,20 @@ def visit_function(self, node): for default in node.args.defaults: try: value = default.infer().next() - except astng.InferenceError: + except astroid.InferenceError: continue - if (isinstance(value, astng.Instance) and - value.qname() in ('__builtin__.set', '__builtin__.dict', '__builtin__.list')): + builtins = bases.BUILTINS + if (isinstance(value, astroid.Instance) and + value.qname() in ['.'.join([builtins, x]) for x in ('set', 'dict', 'list')]): if value is default: msg = default.as_string() - elif type(value) is astng.Instance: + elif type(value) is astroid.Instance: msg = '%s (%s)' % (default.as_string(), value.qname()) else: msg = '%s (%s)' % (default.as_string(), value.as_string()) - self.add_message('W0102', node=node, args=(msg,)) + self.add_message('dangerous-default-value', node=node, args=(msg,)) - @check_messages('W0101', 'W0150') + @check_messages('unreachable', 'lost-exception') def visit_return(self, node): """1 - check is the node has a right sibling (if so, that's some unreachable code) @@ -513,16 +561,16 @@ def visit_return(self, node): """ self._check_unreachable(node) # Is it inside final body of a try...finally bloc ? - self._check_not_in_finally(node, 'return', (astng.Function,)) + self._check_not_in_finally(node, 'return', (astroid.Function,)) - @check_messages('W0101') + @check_messages('unreachable') def visit_continue(self, node): """check is the node has a right sibling (if so, that's some unreachable code) """ self._check_unreachable(node) - @check_messages('W0101', 'W0150') + @check_messages('unreachable', 'lost-exception') def visit_break(self, node): """1 - check is the node has a right sibling (if so, that's some unreachable code) @@ -532,36 +580,42 @@ def visit_break(self, node): # 1 - Is it right sibling ? self._check_unreachable(node) # 2 - Is it inside final body of a try...finally bloc ? - self._check_not_in_finally(node, 'break', (astng.For, astng.While,)) + self._check_not_in_finally(node, 'break', (astroid.For, astroid.While,)) - @check_messages('W0101') + @check_messages('unreachable', 'old-raise-syntax') def visit_raise(self, node): - """check is the node has a right sibling (if so, that's some unreachable + """check if the node has a right sibling (if so, that's some unreachable code) """ self._check_unreachable(node) + if sys.version_info >= (3, 0): + return + if node.exc is not None and node.inst is not None and node.tback is None: + self.add_message('old-raise-syntax', node=node) - @check_messages('W0122') + @check_messages('exec-used') def visit_exec(self, node): """just print a warning on exec statements""" - self.add_message('W0122', node=node) + self.add_message('exec-used', node=node) - @check_messages('W0141', 'W0142') + @check_messages('bad-builtin', 'star-args', 'exec-used') def visit_callfunc(self, node): """visit a CallFunc node -> check if this is not a blacklisted builtin call and check for * or ** use """ - if isinstance(node.func, astng.Name): + if isinstance(node.func, astroid.Name): name = node.func.name # ignore the name if it's not a builtin (i.e. not defined in the # locals nor globals scope) if not (name in node.frame() or name in node.root()): + if name == 'exec': + self.add_message('exec-used', node=node) if name in self.config.bad_functions: - self.add_message('W0141', node=node, args=name) + self.add_message('bad-builtin', node=node, args=name) if node.starargs or node.kwargs: scope = node.scope() - if isinstance(scope, astng.Function): + if isinstance(scope, astroid.Function): toprocess = [(n, vn) for (n, vn) in ((node.starargs, scope.args.vararg), (node.kwargs, scope.args.kwarg)) if n] if toprocess: @@ -569,25 +623,25 @@ def visit_callfunc(self, node): if getattr(cfnode, 'name', None) == fargname: toprocess.remove((cfnode, fargname)) if not toprocess: - return # W0142 can be skipped - self.add_message('W0142', node=node.func) + return # star-args can be skipped + self.add_message('star-args', node=node.func) - @check_messages('W0199') + @check_messages('assert-on-tuple') def visit_assert(self, node): """check the use of an assert statement on a tuple.""" - if node.fail is None and isinstance(node.test, astng.Tuple) and \ + if node.fail is None and isinstance(node.test, astroid.Tuple) and \ len(node.test.elts) == 2: - self.add_message('W0199', node=node) + self.add_message('assert-on-tuple', node=node) - @check_messages('W0109') + @check_messages('duplicate-key') def visit_dict(self, node): """check duplicate key in dictionary""" keys = set() for k, _ in node.items: - if isinstance(k, astng.Const): + if isinstance(k, astroid.Const): key = k.value if key in keys: - self.add_message('W0109', node=node, args=key) + self.add_message('duplicate-key', node=node, args=key) keys.add(key) def visit_tryfinally(self, node): @@ -602,7 +656,7 @@ def _check_unreachable(self, node): """check unreachable code""" unreach_stmt = node.next_sibling() if unreach_stmt is not None: - self.add_message('W0101', node=unreach_stmt) + self.add_message('unreachable', node=unreach_stmt) def _check_not_in_finally(self, node, node_name, breaker_classes=()): """check that a node is not inside a finally clause of a @@ -617,25 +671,24 @@ def _check_not_in_finally(self, node, node_name, breaker_classes=()): _node = node while _parent and not isinstance(_parent, breaker_classes): if hasattr(_parent, 'finalbody') and _node in _parent.finalbody: - self.add_message('W0150', node=node, args=node_name) + self.add_message('lost-exception', node=node, args=node_name) return _node = _parent _parent = _node.parent - class NameChecker(_BasicChecker): msgs = { 'C0102': ('Black listed name "%s"', 'blacklisted-name', 'Used when the name is listed in the black list (unauthorized \ names).'), - 'C0103': ('Invalid name "%s" for type %s (should match %s)', + 'C0103': ('Invalid %s name "%s"', 'invalid-name', 'Used when the name doesn\'t match the regular expression \ associated to its type (constant, variable, class...).'), - } + options = (('module-rgx', {'default' : MOD_NAME_RGX, 'type' :'regexp', 'metavar' : '', @@ -683,6 +736,12 @@ class NameChecker(_BasicChecker): 'help' : 'Regular expression which should only match correct ' 'variable names'} ), + ('class-attribute-rgx', + {'default' : CLASS_ATTRIBUTE_RGX, + 'type' :'regexp', 'metavar' : '', + 'help' : 'Regular expression which should only match correct ' + 'attribute names in class bodies'} + ), ('inlinevar-rgx', {'default' : COMP_VAR_RGX, 'type' :'regexp', 'metavar' : '', @@ -712,48 +771,65 @@ def open(self): badname_const=0, badname_variable=0, badname_inlinevar=0, - badname_argument=0) + badname_argument=0, + badname_class_attribute=0) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): self._check_name('module', node.name.split('.')[-1], node) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') def visit_class(self, node): self._check_name('class', node.name, node) for attr, anodes in node.instance_attrs.iteritems(): - self._check_name('attr', attr, anodes[0]) + if not list(node.instance_attr_ancestors(attr)): + self._check_name('attr', attr, anodes[0]) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') def visit_function(self, node): - self._check_name(node.is_method() and 'method' or 'function', + # Do not emit any warnings if the method is just an implementation + # of a base class method. + if node.is_method() and overrides_a_method(node.parent.frame(), node.name): + return + self._check_name(_determine_function_name_type(node), node.name, node) - # check arguments name + # Check argument names args = node.args.args if args is not None: self._recursive_check_names(args, node) - @check_messages('C0102', 'C0103') + @check_messages('blacklisted-name', 'invalid-name') + def visit_global(self, node): + for name in node.names: + self._check_name('const', name, node) + + @check_messages('blacklisted-name', 'invalid-name') def visit_assname(self, node): """check module level assigned names""" frame = node.frame() ass_type = node.ass_type() - if isinstance(ass_type, (astng.Comprehension, astng.Comprehension)): + if isinstance(ass_type, astroid.Comprehension): self._check_name('inlinevar', node.name, node) - elif isinstance(frame, astng.Module): - if isinstance(ass_type, astng.Assign) and not in_loop(ass_type): - self._check_name('const', node.name, node) - elif isinstance(ass_type, astng.ExceptHandler): + elif isinstance(frame, astroid.Module): + if isinstance(ass_type, astroid.Assign) and not in_loop(ass_type): + if isinstance(safe_infer(ass_type.value), astroid.Class): + self._check_name('class', node.name, node) + else: + self._check_name('const', node.name, node) + elif isinstance(ass_type, astroid.ExceptHandler): self._check_name('variable', node.name, node) - elif isinstance(frame, astng.Function): + elif isinstance(frame, astroid.Function): # global introduced variable aren't in the function locals - if node.name in frame: + if node.name in frame and node.name not in frame.argnames(): self._check_name('variable', node.name, node) + elif isinstance(frame, astroid.Class): + if not list(frame.local_attr_ancestors(node.name)): + self._check_name('class_attribute', node.name, node) def _recursive_check_names(self, args, node): """check names in a possibly recursive list """ for arg in args: - if isinstance(arg, astng.AssName): + if isinstance(arg, astroid.AssName): self._check_name('argument', arg.name, node) else: self._recursive_check_names(arg.elts, node) @@ -768,26 +844,27 @@ def _check_name(self, node_type, name, node): return if name in self.config.bad_names: self.stats['badname_' + node_type] += 1 - self.add_message('C0102', node=node, args=name) + self.add_message('blacklisted-name', node=node, args=name) return regexp = getattr(self.config, node_type + '_rgx') if regexp.match(name) is None: type_label = {'inlinedvar': 'inlined variable', 'const': 'constant', 'attr': 'attribute', + 'class_attribute': 'class attribute' }.get(node_type, node_type) - self.add_message('C0103', node=node, args=(name, type_label, regexp.pattern)) + self.add_message('invalid-name', node=node, args=(type_label, name)) self.stats['badname_' + node_type] += 1 class DocStringChecker(_BasicChecker): msgs = { - 'C0111': ('Missing docstring', # W0131 + 'C0111': ('Missing %s docstring', # W0131 'missing-docstring', 'Used when a module, function, class or method has no docstring.\ Some special methods like __init__ doesn\'t necessary require a \ docstring.'), - 'C0112': ('Empty docstring', # W0132 + 'C0112': ('Empty %s docstring', # W0132 'empty-docstring', 'Used when a module, function, class or method has an empty \ docstring (it would be too easy ;).'), @@ -796,33 +873,41 @@ class DocStringChecker(_BasicChecker): {'default' : NO_REQUIRED_DOC_RGX, 'type' : 'regexp', 'metavar' : '', 'help' : 'Regular expression which should only match ' - 'functions or classes name which do not require a ' - 'docstring'} + 'function or class names that do not require a ' + 'docstring.'} + ), + ('docstring-min-length', + {'default' : -1, + 'type' : 'int', 'metavar' : '', + 'help': ('Minimum line length for functions/classes that' + ' require docstrings, shorter ones are exempt.')} ), ) + def open(self): self.stats = self.linter.add_stats(undocumented_module=0, undocumented_function=0, undocumented_method=0, undocumented_class=0) - + @check_messages('missing-docstring', 'empty-docstring') def visit_module(self, node): self._check_docstring('module', node) + @check_messages('missing-docstring', 'empty-docstring') def visit_class(self, node): if self.config.no_docstring_rgx.match(node.name) is None: self._check_docstring('class', node) - + @check_messages('missing-docstring', 'empty-docstring') def visit_function(self, node): if self.config.no_docstring_rgx.match(node.name) is None: ftype = node.is_method() and 'method' or 'function' - if isinstance(node.parent.frame(), astng.Class): + if isinstance(node.parent.frame(), astroid.Class): overridden = False # check if node is from a method overridden by its ancestor for ancestor in node.parent.frame().ancestors(): if node.name in ancestor and \ - isinstance(ancestor[node.name], astng.Function): + isinstance(ancestor[node.name], astroid.Function): overridden = True break if not overridden: @@ -834,24 +919,32 @@ def _check_docstring(self, node_type, node): """check the node has a non empty docstring""" docstring = node.doc if docstring is None: + if node.body: + lines = node.body[-1].lineno - node.body[0].lineno + 1 + else: + lines = 0 + max_lines = self.config.docstring_min_length + + if node_type != 'module' and max_lines > -1 and lines < max_lines: + return self.stats['undocumented_'+node_type] += 1 - self.add_message('C0111', node=node) + self.add_message('missing-docstring', node=node, args=(node_type,)) elif not docstring.strip(): self.stats['undocumented_'+node_type] += 1 - self.add_message('C0112', node=node) + self.add_message('empty-docstring', node=node, args=(node_type,)) class PassChecker(_BasicChecker): - """check is the pass statement is really necessary""" + """check if the pass statement is really necessary""" msgs = {'W0107': ('Unnecessary pass statement', 'unnecessary-pass', 'Used when a "pass" statement that can be avoided is ' 'encountered.'), } - + @check_messages('unnecessary-pass') def visit_pass(self, node): if len(node.parent.child_sequence(node)) > 1: - self.add_message('W0107', node=node) + self.add_message('unnecessary-pass', node=node) class LambdaForComprehensionChecker(_BasicChecker): @@ -865,23 +958,23 @@ class LambdaForComprehensionChecker(_BasicChecker): 'deprecated-lambda', 'Used when a lambda is the first argument to "map" or ' '"filter". It could be clearer as a list ' - 'comprehension or generator expression.'), + 'comprehension or generator expression.', + {'maxversion': (3, 0)}), } - @check_messages('W0110') + @check_messages('deprecated-lambda') def visit_callfunc(self, node): """visit a CallFunc node, check if map or filter are called with a lambda """ if not node.args: return - if not isinstance(node.args[0], astng.Lambda): + if not isinstance(node.args[0], astroid.Lambda): return infered = safe_infer(node.func) - if (infered - and infered.parent.name == '__builtin__' + if (is_builtin_object(infered) and infered.name in ['map', 'filter']): - self.add_message('W0110', node=node) + self.add_message('deprecated-lambda', node=node) def register(linter): diff --git a/pylibs/pylama/checkers/pylint/checkers/classes.py b/pylibs/pylama/checkers/pylint/checkers/classes.py index 8e3b836d..ecacecea 100644 --- a/pylibs/pylama/checkers/pylint/checkers/classes.py +++ b/pylibs/pylama/checkers/pylint/checkers/classes.py @@ -17,12 +17,12 @@ """ from __future__ import generators -from ..logilab import astng -from ..logilab.astng import YES, Instance, are_exclusive, AssAttr +from .. import astroid +from ..astroid import YES, Instance, are_exclusive, AssAttr -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import (PYMETHODS, overrides_a_method, +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import (PYMETHODS, overrides_a_method, check_messages, is_attr_private, is_attr_protected, node_frame_class) def class_is_abstract(node): @@ -156,7 +156,7 @@ class ClassChecker(BaseChecker): * unreachable code """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'classes' @@ -222,7 +222,7 @@ def visit_class(self, node): if node.type == 'class': try: node.local_attr('__init__') - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('W0232', args=node, node=node) @check_messages('E0203', 'W0201') @@ -241,7 +241,7 @@ def leave_class(self, cnode): defining_methods = self.config.defining_attr_methods for attr, nodes in cnode.instance_attrs.iteritems(): nodes = [n for n in nodes if not - isinstance(n.statement(), (astng.Delete, astng.AugAssign))] + isinstance(n.statement(), (astroid.Delete, astroid.AugAssign))] if not nodes: continue # error detected by typechecking attr_defined = False @@ -264,7 +264,7 @@ def leave_class(self, cnode): # check attribute is defined as a class attribute try: cnode.local_attr(attr) - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('W0201', args=attr, node=node) def visit_function(self, node): @@ -281,25 +281,25 @@ def visit_function(self, node): return # check signature if the method overloads inherited method for overridden in klass.local_attr_ancestors(node.name): - # get astng for the searched method + # get astroid for the searched method try: meth_node = overridden[node.name] except KeyError: # we have found the method but it's not in the local # dictionary. - # This may happen with astng build from living objects + # This may happen with astroid build from living objects continue - if not isinstance(meth_node, astng.Function): + if not isinstance(meth_node, astroid.Function): continue self._check_signature(node, meth_node, 'overridden') break if node.decorators: for decorator in node.decorators.nodes: - if isinstance(decorator, astng.Getattr) and \ + if isinstance(decorator, astroid.Getattr) and \ decorator.attrname in ('getter', 'setter', 'deleter'): # attribute affectation will call this method, not hiding it return - if isinstance(decorator, astng.Name) and decorator.name == 'property': + if isinstance(decorator, astroid.Name) and decorator.name == 'property': # attribute affectation will either call a setter or raise # an attribute error, anyway not hiding the function return @@ -308,7 +308,7 @@ def visit_function(self, node): overridden = klass.instance_attr(node.name)[0] # XXX args = (overridden.root().name, overridden.fromlineno) self.add_message('E0202', args=args, node=node) - except astng.NotFoundError: + except astroid.NotFoundError: pass def leave_function(self, node): @@ -387,8 +387,8 @@ def _check_protected_attribute_access(self, node): return # If the expression begins with a call to super, that's ok. - if isinstance(node.expr, astng.CallFunc) and \ - isinstance(node.expr.func, astng.Name) and \ + if isinstance(node.expr, astroid.CallFunc) and \ + isinstance(node.expr.func, astroid.Name) and \ node.expr.func.name == 'super': return @@ -416,7 +416,7 @@ def _check_accessed_members(self, node, accessed): node.local_attr(attr) # yes, stop here continue - except astng.NotFoundError: + except astroid.NotFoundError: pass # is it an instance attribute of a parent class ? try: @@ -428,7 +428,7 @@ def _check_accessed_members(self, node, accessed): # is it an instance attribute ? try: defstmts = node.instance_attr(attr) - except astng.NotFoundError: + except astroid.NotFoundError: pass else: if len(defstmts) == 1: @@ -530,7 +530,7 @@ def _check_interfaces(self, node): e0221_hack = [False] def iface_handler(obj): """filter interface objects, it should be classes""" - if not isinstance(obj, astng.Class): + if not isinstance(obj, astroid.Class): e0221_hack[0] = True self.add_message('E0221', node=node, args=(obj.as_string(),)) @@ -545,10 +545,10 @@ def iface_handler(obj): # don't check method beginning with an underscore, # usually belonging to the interface implementation continue - # get class method astng + # get class method astroid try: method = node_method(node, name) - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('E0222', args=(name, iface.name), node=node) continue @@ -558,12 +558,12 @@ def iface_handler(obj): # check signature self._check_signature(method, imethod, '%s interface' % iface.name) - except astng.InferenceError: + except astroid.InferenceError: if e0221_hack[0]: return implements = Instance(node).getattr('__implements__')[0] assignment = implements.parent - assert isinstance(assignment, astng.Assign) + assert isinstance(assignment, astroid.Assign) # assignment.expr can be a Name or a Tuple or whatever. # Use as_string() for the message # FIXME: in case of multiple interfaces, find which one could not @@ -580,14 +580,14 @@ def _check_init(self, node): klass_node = node.parent.frame() to_call = _ancestors_to_call(klass_node) not_called_yet = dict(to_call) - for stmt in node.nodes_of_class(astng.CallFunc): + for stmt in node.nodes_of_class(astroid.CallFunc): expr = stmt.func - if not isinstance(expr, astng.Getattr) \ + if not isinstance(expr, astroid.Getattr) \ or expr.attrname != '__init__': continue # skip the test if using super - if isinstance(expr.expr, astng.CallFunc) and \ - isinstance(expr.expr.func, astng.Name) and \ + if isinstance(expr.expr, astroid.CallFunc) and \ + isinstance(expr.expr.func, astroid.Name) and \ expr.expr.func.name == 'super': return try: @@ -599,7 +599,7 @@ def _check_init(self, node): except KeyError: if klass not in to_call: self.add_message('W0233', node=expr, args=klass.name) - except astng.InferenceError: + except astroid.InferenceError: continue for klass, method in not_called_yet.iteritems(): if klass.name == 'object' or method.parent.name == 'object': @@ -611,8 +611,8 @@ def _check_signature(self, method1, refmethod, class_type): class_type is in 'class', 'interface' """ - if not (isinstance(method1, astng.Function) - and isinstance(refmethod, astng.Function)): + if not (isinstance(method1, astroid.Function) + and isinstance(refmethod, astroid.Function)): self.add_message('F0202', args=(method1, refmethod), node=method1) return # don't care about functions with unknown argument (builtins) @@ -632,7 +632,7 @@ def is_first_attr(self, node): """Check that attribute lookup name use first attribute variable name (self for method, cls for classmethod and mcs for metaclass). """ - return self._first_attrs and isinstance(node.expr, astng.Name) and \ + return self._first_attrs and isinstance(node.expr, astroid.Name) and \ node.expr.name == self._first_attrs[-1] def _ancestors_to_call(klass_node, method='__init__'): @@ -643,19 +643,19 @@ def _ancestors_to_call(klass_node, method='__init__'): for base_node in klass_node.ancestors(recurs=False): try: to_call[base_node] = base_node.igetattr(method).next() - except astng.InferenceError: + except astroid.InferenceError: continue return to_call def node_method(node, method_name): - """get astng for on the given class node, ensuring it + """get astroid for on the given class node, ensuring it is a Function node """ for n in node.local_attr(method_name): - if isinstance(n, astng.Function): + if isinstance(n, astroid.Function): return n - raise astng.NotFoundError(method_name) + raise astroid.NotFoundError(method_name) def register(linter): """required method to auto register this checker """ diff --git a/pylibs/pylama/checkers/pylint/checkers/design_analysis.py b/pylibs/pylama/checkers/pylint/checkers/design_analysis.py index 789cf88f..f81efc37 100644 --- a/pylibs/pylama/checkers/pylint/checkers/design_analysis.py +++ b/pylibs/pylama/checkers/pylint/checkers/design_analysis.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -13,17 +13,13 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""check for signs of poor design +"""check for signs of poor design""" +from ..astroid import Function, If, InferenceError - see http://intranet.logilab.fr/jpl/view?rql=Any%20X%20where%20X%20eid%201243 - FIXME: missing 13, 15, 16 -""" - -from ..logilab.astng import Function, If, InferenceError - -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import check_messages import re @@ -33,10 +29,9 @@ SPECIAL_METHODS = [('Context manager', set(('__enter__', '__exit__',))), ('Container', set(('__len__', - '__getitem__', - '__setitem__', - '__delitem__',))), - ('Callable', set(('__call__',))), + '__getitem__',))), + ('Mutable container', set(('__setitem__', + '__delitem__',))), ] class SpecialMethodChecker(object): @@ -60,8 +55,8 @@ def __call__(self, methods_required, protocol): required_methods_found = methods_required & self.methods_found if required_methods_found == methods_required: return True - if required_methods_found != set(): - required_methods_missing = methods_required - self.methods_found + if required_methods_found: + required_methods_missing = methods_required - self.methods_found self.on_error((protocol, ', '.join(sorted(required_methods_found)), ', '.join(sorted(required_methods_missing)))) @@ -83,11 +78,11 @@ def class_is_abstract(klass): 'R0901': ('Too many ancestors (%s/%s)', 'too-many-ancestors', 'Used when class has too many parent classes, try to reduce \ - this to get a more simple (and so easier to use) class.'), + this to get a simpler (and so easier to use) class.'), 'R0902': ('Too many instance attributes (%s/%s)', 'too-many-instance-attributes', 'Used when class has too many instance attributes, try to reduce \ - this to get a more simple (and so easier to use) class.'), + this to get a simpler (and so easier to use) class.'), 'R0903': ('Too few public methods (%s/%s)', 'too-few-public-methods', 'Used when class has too few public methods, so be sure it\'s \ @@ -95,7 +90,7 @@ def class_is_abstract(klass): 'R0904': ('Too many public methods (%s/%s)', 'too-many-public-methods', 'Used when class has too many public methods, try to reduce \ - this to get a more simple (and so easier to use) class.'), + this to get a simpler (and so easier to use) class.'), 'R0911': ('Too many return statements (%s/%s)', 'too-many-return-statements', @@ -139,7 +134,7 @@ class MisdesignChecker(BaseChecker): * size, complexity of functions, methods """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'design' @@ -166,7 +161,7 @@ class MisdesignChecker(BaseChecker): 'help': 'Maximum number of return / yield for function / ' 'method body'} ), - ('max-branchs', + ('max-branches', {'default' : 12, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of branch for function / method body'} ), @@ -208,7 +203,7 @@ def __init__(self, linter=None): BaseChecker.__init__(self, linter) self.stats = None self._returns = None - self._branchs = None + self._branches = None self._used_abstracts = None self._used_ifaces = None self._abstracts = None @@ -219,12 +214,13 @@ def open(self): """initialize visit variables""" self.stats = self.linter.add_stats() self._returns = [] - self._branchs = [] + self._branches = [] self._used_abstracts = {} self._used_ifaces = {} self._abstracts = [] self._ifaces = [] + # Check 'R0921', 'R0922', 'R0923' def close(self): """check that abstract/interface classes are used""" for abstract in self._abstracts: @@ -237,6 +233,7 @@ def close(self): if not iface in self._used_ifaces: self.add_message('R0923', node=iface) + @check_messages('R0901', 'R0902', 'R0903', 'R0904', 'R0921', 'R0922', 'R0923') def visit_class(self, node): """check size of inheritance hierarchy and number of instance attributes """ @@ -274,6 +271,7 @@ def visit_class(self, node): except KeyError: self._used_abstracts[parent] = 1 + @check_messages('R0901', 'R0902', 'R0903', 'R0904', 'R0921', 'R0922', 'R0923') def leave_class(self, node): """check number of public methods""" nb_public_methods = 0 @@ -304,6 +302,7 @@ def leave_class(self, node): args=(nb_public_methods, self.config.min_public_methods)) + @check_messages('R0911', 'R0912', 'R0913', 'R0914', 'R0915') def visit_function(self, node): """check function name, docstring, arguments, redefinition, variable names, max locals @@ -311,7 +310,7 @@ def visit_function(self, node): self._inc_branch() # init branch and returns counters self._returns.append(0) - self._branchs.append(0) + self._branches.append(0) # check number of arguments args = node.args.args if args is not None: @@ -332,6 +331,7 @@ def visit_function(self, node): # init statements counter self._stmts = 1 + @check_messages('R0911', 'R0912', 'R0913', 'R0914', 'R0915') def leave_function(self, node): """most of the work is done here on close: checks for max returns, branch, return in __init__ @@ -340,10 +340,10 @@ def leave_function(self, node): if returns > self.config.max_returns: self.add_message('R0911', node=node, args=(returns, self.config.max_returns)) - branchs = self._branchs.pop() - if branchs > self.config.max_branchs: + branches = self._branches.pop() + if branches > self.config.max_branches: self.add_message('R0912', node=node, - args=(branchs, self.config.max_branchs)) + args=(branches, self.config.max_branches)) # check number of statements if self._stmts > self.config.max_statements: self.add_message('R0915', node=node, @@ -363,42 +363,42 @@ def visit_default(self, node): self._stmts += 1 def visit_tryexcept(self, node): - """increments the branchs counter""" - branchs = len(node.handlers) + """increments the branches counter""" + branches = len(node.handlers) if node.orelse: - branchs += 1 - self._inc_branch(branchs) - self._stmts += branchs + branches += 1 + self._inc_branch(branches) + self._stmts += branches def visit_tryfinally(self, _): - """increments the branchs counter""" + """increments the branches counter""" self._inc_branch(2) self._stmts += 2 def visit_if(self, node): - """increments the branchs counter""" - branchs = 1 + """increments the branches counter""" + branches = 1 # don't double count If nodes coming from some 'elif' if node.orelse and (len(node.orelse)>1 or not isinstance(node.orelse[0], If)): - branchs += 1 - self._inc_branch(branchs) - self._stmts += branchs + branches += 1 + self._inc_branch(branches) + self._stmts += branches def visit_while(self, node): - """increments the branchs counter""" - branchs = 1 + """increments the branches counter""" + branches = 1 if node.orelse: - branchs += 1 - self._inc_branch(branchs) + branches += 1 + self._inc_branch(branches) visit_for = visit_while - def _inc_branch(self, branchsnum=1): - """increments the branchs counter""" - branchs = self._branchs - for i in xrange(len(branchs)): - branchs[i] += branchsnum + def _inc_branch(self, branchesnum=1): + """increments the branches counter""" + branches = self._branches + for i in xrange(len(branches)): + branches[i] += branchesnum # FIXME: make a nice report... diff --git a/pylibs/pylama/checkers/pylint/checkers/exceptions.py b/pylibs/pylama/checkers/pylint/checkers/exceptions.py index b2006cef..3031075f 100644 --- a/pylibs/pylama/checkers/pylint/checkers/exceptions.py +++ b/pylibs/pylama/checkers/pylint/checkers/exceptions.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2007 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software @@ -18,12 +18,12 @@ from ..logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ -from ..logilab import astng -from ..logilab.astng import YES, Instance, unpack_infer +from .. import astroid +from ..astroid import YES, Instance, unpack_infer -from ..checkers import BaseChecker -from ..checkers.utils import is_empty, is_raising -from ..interfaces import IASTNGChecker +from . import BaseChecker +from .utils import is_empty, is_raising, check_messages +from ..interfaces import IAstroidChecker OVERGENERAL_EXCEPTIONS = ('Exception',) @@ -71,6 +71,12 @@ 'Used when the exception to catch is of the form \ "except A or B:". If intending to catch multiple, \ rewrite as "except (A, B):"'), + 'W0712': ('Implicit unpacking of exceptions is not supported in Python 3', + 'unpacking-in-except', + 'Python3 will not allow implicit unpacking of exceptions in except ' + 'clauses. ' + 'See http://www.python.org/dev/peps/pep-3110/', + {'maxversion': (3, 0)}), } @@ -85,7 +91,7 @@ class ExceptionsChecker(BaseChecker): * type of raise argument : string, Exceptions, other values """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'exceptions' msgs = MSGS @@ -99,6 +105,7 @@ class ExceptionsChecker(BaseChecker): ), ) + @check_messages('W0701', 'W0710', 'E0702', 'E0710', 'E0711') def visit_raise(self, node): """visit raise possibly inferring value""" # ignore empty raise @@ -110,7 +117,7 @@ def visit_raise(self, node): else: try: value = unpack_infer(expr).next() - except astng.InferenceError: + except astroid.InferenceError: return self._check_raise_value(node, value) @@ -118,29 +125,29 @@ def _check_raise_value(self, node, expr): """check for bad values, string exception and class inheritance """ value_found = True - if isinstance(expr, astng.Const): + if isinstance(expr, astroid.Const): value = expr.value if isinstance(value, str): self.add_message('W0701', node=node) else: self.add_message('E0702', node=node, args=value.__class__.__name__) - elif (isinstance(expr, astng.Name) and \ + elif (isinstance(expr, astroid.Name) and \ expr.name in ('None', 'True', 'False')) or \ - isinstance(expr, (astng.List, astng.Dict, astng.Tuple, - astng.Module, astng.Function)): + isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple, + astroid.Module, astroid.Function)): self.add_message('E0702', node=node, args=expr.name) - elif ( (isinstance(expr, astng.Name) and expr.name == 'NotImplemented') - or (isinstance(expr, astng.CallFunc) and - isinstance(expr.func, astng.Name) and + elif ( (isinstance(expr, astroid.Name) and expr.name == 'NotImplemented') + or (isinstance(expr, astroid.CallFunc) and + isinstance(expr.func, astroid.Name) and expr.func.name == 'NotImplemented') ): self.add_message('E0711', node=node) - elif isinstance(expr, astng.BinOp) and expr.op == '%': + elif isinstance(expr, astroid.BinOp) and expr.op == '%': self.add_message('W0701', node=node) - elif isinstance(expr, (Instance, astng.Class)): + elif isinstance(expr, (Instance, astroid.Class)): if isinstance(expr, Instance): expr = expr._proxied - if (isinstance(expr, astng.Class) and + if (isinstance(expr, astroid.Class) and not inherit_from_std_ex(expr) and expr.root().name != BUILTINS_NAME): if expr.newstyle: @@ -154,6 +161,12 @@ def _check_raise_value(self, node, expr): return value_found + @check_messages('W0712') + def visit_excepthandler(self, node): + """Visit an except handler block and check for exception unpacking.""" + if isinstance(node.name, (astroid.Tuple, astroid.List)): + self.add_message('W0712', node=node) + @check_messages('W0702', 'W0703', 'W0704', 'W0711', 'E0701') def visit_tryexcept(self, node): """check for empty except""" exceptions_classes = [] @@ -171,19 +184,19 @@ def visit_tryexcept(self, node): msg = 'empty except clause should always appear last' self.add_message('E0701', node=node, args=msg) - elif isinstance(handler.type, astng.BoolOp): + elif isinstance(handler.type, astroid.BoolOp): self.add_message('W0711', node=handler, args=handler.type.op) else: try: excs = list(unpack_infer(handler.type)) - except astng.InferenceError: + except astroid.InferenceError: continue for exc in excs: # XXX skip other non class nodes - if exc is YES or not isinstance(exc, astng.Class): + if exc is YES or not isinstance(exc, astroid.Class): continue exc_ancestors = [anc for anc in exc.ancestors() - if isinstance(anc, astng.Class)] + if isinstance(anc, astroid.Class)] for previous_exc in exceptions_classes: if previous_exc in exc_ancestors: msg = '%s is an ancestor class of %s' % ( diff --git a/pylibs/pylama/checkers/pylint/checkers/format.py b/pylibs/pylama/checkers/pylint/checkers/format.py index 83da2613..bf53234a 100644 --- a/pylibs/pylama/checkers/pylint/checkers/format.py +++ b/pylibs/pylama/checkers/pylint/checkers/format.py @@ -27,12 +27,12 @@ raise ValueError("tokenize.NL doesn't exist -- tokenize module too old") from ..logilab.common.textutils import pretty_match -from ..logilab.astng import nodes +from ..astroid import nodes -from ..interfaces import IRawChecker, IASTNGChecker -from ..checkers import BaseRawChecker -from ..checkers.utils import check_messages -from ..utils import WarningScope +from ..interfaces import ITokenChecker, IAstroidChecker +from . import BaseTokenChecker +from .utils import check_messages +from ..utils import WarningScope, OPTION_RGX MSGS = { 'C0301': ('Line too long (%s/%s)', @@ -42,7 +42,13 @@ 'too-many-lines', 'Used when a module has too much lines, reducing its readability.' ), - + 'C0303': ('Trailing whitespace', + 'trailing-whitespace', + 'Used when there is whitespace between the end of a line and the ' + 'newline.'), + 'C0304': ('Final newline missing', + 'missing-final-newline', + 'Used when the last line in a file is missing a newline.'), 'W0311': ('Bad indentation. Found %s %s, expected %s', 'bad-indentation', 'Used when an unexpected number of indentation\'s tabulations or ' @@ -163,7 +169,7 @@ def check_line(line): return msg_id, pretty_match(match, line.rstrip()) -class FormatChecker(BaseRawChecker): +class FormatChecker(BaseTokenChecker): """checks for : * unauthorized constructions * strict indentation @@ -171,7 +177,7 @@ class FormatChecker(BaseRawChecker): * use of <> instead of != """ - __implements__ = (IRawChecker, IASTNGChecker) + __implements__ = (ITokenChecker, IAstroidChecker) # configuration section name name = 'format' @@ -182,6 +188,11 @@ class FormatChecker(BaseRawChecker): options = (('max-line-length', {'default' : 80, 'type' : "int", 'metavar' : '', 'help' : 'Maximum number of characters on a single line.'}), + ('ignore-long-lines', + {'type': 'regexp', 'metavar': '', + 'default': r'^\s*(# )??$', + 'help': ('Regexp for a line that is allowed to be longer than ' + 'the limit.')}), ('max-module-lines', {'default' : 1000, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of lines in a module'} @@ -192,22 +203,10 @@ class FormatChecker(BaseRawChecker): " " (4 spaces) or "\\t" (1 tab).'}), ) def __init__(self, linter=None): - BaseRawChecker.__init__(self, linter) + BaseTokenChecker.__init__(self, linter) self._lines = None self._visited_lines = None - def process_module(self, node): - """extracts encoding from the stream and decodes each line, so that - international text's length is properly calculated. - """ - stream = node.file_stream - stream.seek(0) # XXX may be removed with astng > 0.23 - readline = stream.readline - if sys.version_info < (3, 0): - if node.file_encoding is not None: - readline = lambda: stream.readline().decode(node.file_encoding, 'replace') - self.process_tokens(tokenize.generate_tokens(readline)) - def new_line(self, tok_type, line, line_num, junk): """a new line has been encountered, process it if necessary""" if not tok_type in junk: @@ -233,13 +232,24 @@ def process_tokens(self, tokens): previous = None self._lines = {} self._visited_lines = {} + new_line_delay = False for (tok_type, token, start, _, line) in tokens: + if new_line_delay: + new_line_delay = False + self.new_line(tok_type, line, line_num, junk) if start[0] != line_num: if previous is not None and previous[0] == tokenize.OP and previous[1] == ';': self.add_message('W0301', line=previous[2]) previous = None line_num = start[0] - self.new_line(tok_type, line, line_num, junk) + # A tokenizer oddity: if an indented line contains a multi-line + # docstring, the line member of the INDENT token does not contain + # the full line; therefore we delay checking the new line until + # the next token. + if tok_type == tokenize.INDENT: + new_line_delay = True + else: + self.new_line(tok_type, line, line_num, junk) if tok_type not in (indent, dedent, newline) + junk: previous = tok_type, token, start[0] @@ -338,8 +348,22 @@ def check_lines(self, lines, i): """check lines have less than a maximum number of characters """ max_chars = self.config.max_line_length - for line in lines.splitlines(): - if len(line) > max_chars: + ignore_long_line = self.config.ignore_long_lines + + for line in lines.splitlines(True): + if not line.endswith('\n'): + self.add_message('C0304', line=i) + else: + stripped_line = line.rstrip() + if line != stripped_line + '\n': + self.add_message('C0303', line=i) + # Don't count excess whitespace in the line length. + line = stripped_line + mobj = OPTION_RGX.search(line) + if mobj and mobj.group(1).split('=', 1)[0].strip() == 'disable': + line = line.split('#')[0].rstrip() + + if len(line) > max_chars and not ignore_long_line.search(line): self.add_message('C0301', line=i, args=(len(line), max_chars)) i += 1 diff --git a/pylibs/pylama/checkers/pylint/checkers/imports.py b/pylibs/pylama/checkers/pylint/checkers/imports.py index e243ff6b..3c321bbb 100644 --- a/pylibs/pylama/checkers/pylint/checkers/imports.py +++ b/pylibs/pylama/checkers/pylint/checkers/imports.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -19,11 +19,13 @@ from ..logilab.common.modutils import is_standard_module from ..logilab.common.ureports import VerbatimText, Paragraph -from ..logilab import astng -from ..logilab.astng import are_exclusive +from .. import astroid +from ..astroid import are_exclusive -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker, EmptyReport +from ..interfaces import IAstroidChecker +from ..utils import EmptyReport +from . import BaseChecker +from .utils import check_messages def get_first_import(node, context, name, base, level): @@ -38,11 +40,11 @@ def get_first_import(node, context, name, base, level): continue if first.scope() is node.scope() and first.fromlineno > node.fromlineno: continue - if isinstance(first, astng.Import): + if isinstance(first, astroid.Import): if any(fullname == iname[0] for iname in first.names): found = True break - elif isinstance(first, astng.From): + elif isinstance(first, astroid.From): if level == first.level and any( fullname == '%s.%s' % (first.modname, iname[0]) for iname in first.names): found = True @@ -157,15 +159,14 @@ class ImportsChecker(BaseChecker): * uses of deprecated modules """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'imports' msgs = MSGS priority = -2 options = (('deprecated-modules', - {'default' : ('regsub', 'string', 'TERMIOS', - 'Bastion', 'rexec'), + {'default' : ('regsub', 'TERMIOS', 'Bastion', 'rexec'), 'type' : 'csv', 'metavar' : '', 'help' : 'Deprecated modules which should not be used, \ @@ -232,7 +233,9 @@ def visit_import(self, node): self._check_deprecated_module(node, name) self._check_reimport(node, name) - + # TODO This appears to be the list of all messages of the checker... + # @check_messages('W0410', 'W0401', 'W0403', 'W0402', 'W0404', 'W0406', 'F0401') + @check_messages(*(MSGS.keys())) def visit_from(self, node): """triggered when a from statement is seen""" basename = node.modname @@ -241,7 +244,7 @@ def visit_from(self, node): prev = node.previous_sibling() if prev: # consecutive future statements are possible - if not (isinstance(prev, astng.From) + if not (isinstance(prev, astroid.From) and prev.modname == '__future__'): self.add_message('W0410', node=node) return @@ -261,7 +264,7 @@ def visit_from(self, node): def get_imported_module(self, modnode, importnode, modname): try: return importnode.do_import_module(modname) - except astng.InferenceError, ex: + except astroid.InferenceError, ex: if str(ex) != modname: args = '%r (%s)' % (modname, ex) else: diff --git a/pylibs/pylama/checkers/pylint/checkers/logging.py b/pylibs/pylama/checkers/pylint/checkers/logging.py index bb95a539..5f2381ca 100644 --- a/pylibs/pylama/checkers/pylint/checkers/logging.py +++ b/pylibs/pylama/checkers/pylint/checkers/logging.py @@ -14,11 +14,9 @@ """checker for use of Python logging """ -from ..logilab import astng -from .. import checkers +from .. import astroid +from . import BaseChecker, utils from .. import interfaces -from ..checkers import utils - MSGS = { 'W1201': ('Specify string format arguments as logging function parameters', @@ -54,10 +52,10 @@ 'warning']) -class LoggingChecker(checkers.BaseChecker): +class LoggingChecker(BaseChecker): """Checks use of the logging module.""" - __implements__ = interfaces.IASTNGChecker + __implements__ = interfaces.IAstroidChecker name = 'logging' msgs = MSGS @@ -77,18 +75,19 @@ def visit_import(self, node): else: self._logging_name = 'logging' + @utils.check_messages(*(MSGS.keys())) def visit_callfunc(self, node): """Checks calls to (simple forms of) logging methods.""" - if (not isinstance(node.func, astng.Getattr) - or not isinstance(node.func.expr, astng.Name)): + if (not isinstance(node.func, astroid.Getattr) + or not isinstance(node.func.expr, astroid.Name)): return try: logger_class = [inferred for inferred in node.func.expr.infer() if ( - isinstance(inferred, astng.Instance) + isinstance(inferred, astroid.Instance) and any(ancestor for ancestor in inferred._proxied.ancestors() if ( ancestor.name == 'Logger' and ancestor.parent.name == 'logging')))] - except astng.exceptions.InferenceError: + except astroid.exceptions.InferenceError: return if (node.func.expr.name != self._logging_name and not logger_class): return @@ -103,9 +102,9 @@ def _check_convenience_methods(self, node): # Either no args, star args, or double-star args. Beyond the # scope of this checker. return - if isinstance(node.args[0], astng.BinOp) and node.args[0].op == '%': + if isinstance(node.args[0], astroid.BinOp) and node.args[0].op == '%': self.add_message('W1201', node=node) - elif isinstance(node.args[0], astng.Const): + elif isinstance(node.args[0], astroid.Const): self._check_format_string(node, 0) def _check_log_methods(self, node): @@ -116,9 +115,9 @@ def _check_log_methods(self, node): # Either a malformed call, star args, or double-star args. Beyond # the scope of this checker. return - if isinstance(node.args[1], astng.BinOp) and node.args[1].op == '%': + if isinstance(node.args[1], astroid.BinOp) and node.args[1].op == '%': self.add_message('W1201', node=node) - elif isinstance(node.args[1], astng.Const): + elif isinstance(node.args[1], astroid.Const): self._check_format_string(node, 1) def _check_format_string(self, node, format_arg): @@ -171,7 +170,7 @@ def _count_supplied_tokens(self, args): Returns: Number of AST nodes that aren't keywords. """ - return sum(1 for arg in args if not isinstance(arg, astng.Keyword)) + return sum(1 for arg in args if not isinstance(arg, astroid.Keyword)) def register(linter): diff --git a/pylibs/pylama/checkers/pylint/checkers/misc.py b/pylibs/pylama/checkers/pylint/checkers/misc.py index 8830b8fd..dee53e62 100644 --- a/pylibs/pylama/checkers/pylint/checkers/misc.py +++ b/pylibs/pylama/checkers/pylint/checkers/misc.py @@ -20,19 +20,25 @@ import re from ..interfaces import IRawChecker -from ..checkers import BaseChecker +from . import BaseChecker MSGS = { 'W0511': ('%s', 'fixme', 'Used when a warning note as FIXME or XXX is detected.'), + 'W0512': ('Cannot decode using encoding "%s", unexpected byte at position %d', + 'invalid-encoded-data', + 'Used when a source line cannot be decoded using the specified ' + 'source file encoding.', + {'maxversion': (3, 0)}), } + class EncodingChecker(BaseChecker): """checks for: * warning notes in the code like FIXME, XXX - * PEP 263: source code with non ascii character but no encoding declaration + * encoding issues. """ __implements__ = IRawChecker @@ -48,30 +54,36 @@ class EncodingChecker(BaseChecker): }), ) - def __init__(self, linter=None): - BaseChecker.__init__(self, linter) + def _check_note(self, notes, lineno, line): + match = notes.search(line) + if match: + self.add_message('W0511', args=line[match.start():-1], line=lineno) + + def _check_encoding(self, lineno, line, file_encoding): + try: + return unicode(line, file_encoding) + except UnicodeDecodeError, ex: + self.add_message('W0512', line=lineno, + args=(file_encoding, ex.args[2])) - def process_module(self, node): - """inspect the source file to found encoding problem or fixmes like + def process_module(self, module): + """inspect the source file to find encoding problem or fixmes like notes """ - stream = node.file_stream - stream.seek(0) # XXX may be removed with astng > 0.23 - # warning notes in the code - notes = [] - for note in self.config.notes: - notes.append(re.compile(note)) - linenum = 1 - for line in stream.readlines(): - for note in notes: - match = note.search(line) - if match: - self.add_message('W0511', args=line[match.start():-1], - line=linenum) - break - linenum += 1 - - + stream = module.file_stream + stream.seek(0) # XXX may be removed with astroid > 0.23 + if self.config.notes: + notes = re.compile('|'.join(self.config.notes)) + else: + notes = None + if module.file_encoding: + encoding = module.file_encoding + else: + encoding = 'ascii' + for lineno, line in enumerate(stream): + line = self._check_encoding(lineno+1, line, encoding) + if line is not None and notes: + self._check_note(notes, lineno+1, line) def register(linter): """required method to auto register this checker""" diff --git a/pylibs/pylama/checkers/pylint/checkers/newstyle.py b/pylibs/pylama/checkers/pylint/checkers/newstyle.py index c72f8d0d..715282a9 100644 --- a/pylibs/pylama/checkers/pylint/checkers/newstyle.py +++ b/pylibs/pylama/checkers/pylint/checkers/newstyle.py @@ -15,12 +15,13 @@ # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """check for new / old style related problems """ +import sys -from ..logilab import astng +from .. import astroid -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import check_messages +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import check_messages MSGS = { 'E1001': ('Use of __slots__ on an old style class', @@ -29,15 +30,23 @@ 'E1002': ('Use of super on an old style class', 'super-on-old-class', 'Used when an old style class uses the super builtin.'), - 'E1003': ('Bad first argument %r given to super class', + 'E1003': ('Bad first argument %r given to super()', 'bad-super-call', 'Used when another argument than the current class is given as \ first argument of the super builtin.'), + 'E1004': ('Missing argument to super()', + 'missing-super-argument', + 'Used when the super builtin didn\'t receive an \ + argument on Python 2'), 'W1001': ('Use of "property" on an old style class', 'property-on-old-class', 'Used when PyLint detect the use of the builtin "property" \ on an old style class while this is relying on new style \ classes features'), + 'C1001': ('Old-style class defined.', + 'old-style-class', + 'Used when a class is defined that does not inherit from another' + 'class and does not inherit explicitly from "object".') } @@ -48,7 +57,7 @@ class NewStyleConflictChecker(BaseChecker): * "super" usage """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'newstyle' @@ -58,53 +67,67 @@ class NewStyleConflictChecker(BaseChecker): # configuration options options = () - @check_messages('E1001') + @check_messages('E1001', 'C1001') def visit_class(self, node): """check __slots__ usage """ if '__slots__' in node and not node.newstyle: self.add_message('E1001', node=node) + # The node type could be class, exception, metaclass, or + # interface. Presumably, the non-class-type nodes would always + # have an explicit base class anyway. + if not node.bases and node.type == 'class': + self.add_message('C1001', node=node) @check_messages('W1001') def visit_callfunc(self, node): """check property usage""" parent = node.parent.frame() - if (isinstance(parent, astng.Class) and + if (isinstance(parent, astroid.Class) and not parent.newstyle and - isinstance(node.func, astng.Name)): + isinstance(node.func, astroid.Name)): name = node.func.name if name == 'property': self.add_message('W1001', node=node) - @check_messages('E1002', 'E1003') + @check_messages('E1002', 'E1003', 'E1004') def visit_function(self, node): """check use of super""" # ignore actual functions or method within a new style class if not node.is_method(): return klass = node.parent.frame() - for stmt in node.nodes_of_class(astng.CallFunc): + for stmt in node.nodes_of_class(astroid.CallFunc): expr = stmt.func - if not isinstance(expr, astng.Getattr): + if not isinstance(expr, astroid.Getattr): continue call = expr.expr # skip the test if using super - if isinstance(call, astng.CallFunc) and \ - isinstance(call.func, astng.Name) and \ + if isinstance(call, astroid.CallFunc) and \ + isinstance(call.func, astroid.Name) and \ call.func.name == 'super': if not klass.newstyle: # super should not be used on an old style class self.add_message('E1002', node=node) else: # super first arg should be the class + if not call.args and sys.version_info[0] == 3: + # unless Python 3 + continue + try: supcls = (call.args and call.args[0].infer().next() or None) - except astng.InferenceError: + except astroid.InferenceError: continue + + if supcls is None and sys.version_info[0] == 2: + self.add_message('missing-super-argument', node=call) + continue + if klass is not supcls: supcls = getattr(supcls, 'name', supcls) - self.add_message('E1003', node=node, args=supcls) + self.add_message('E1003', node=call, args=(supcls, )) def register(linter): diff --git a/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py b/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py index 9f270357..4d450da1 100644 --- a/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py +++ b/pylibs/pylama/checkers/pylint/checkers/raw_metrics.py @@ -1,3 +1,6 @@ +# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later @@ -24,8 +27,9 @@ from ..logilab.common.ureports import Table -from ..interfaces import IRawChecker -from ..checkers import BaseRawChecker, EmptyReport +from ..interfaces import ITokenChecker +from ..utils import EmptyReport +from ..checkers import BaseTokenChecker from ..reporters import diff_string def report_raw_stats(sect, stats, old_stats): @@ -50,7 +54,7 @@ def report_raw_stats(sect, stats, old_stats): sect.append(Table(children=lines, cols=5, rheaders=1)) -class RawMetricsChecker(BaseRawChecker): +class RawMetricsChecker(BaseTokenChecker): """does not check anything but gives some raw metrics : * total number of lines * total number of code lines @@ -59,7 +63,7 @@ class RawMetricsChecker(BaseRawChecker): * total number of empty lines """ - __implements__ = (IRawChecker,) + __implements__ = (ITokenChecker,) # configuration section name name = 'metrics' @@ -71,7 +75,7 @@ class RawMetricsChecker(BaseRawChecker): reports = ( ('RP0701', 'Raw metrics', report_raw_stats), ) def __init__(self, linter): - BaseRawChecker.__init__(self, linter) + BaseTokenChecker.__init__(self, linter) self.stats = None def open(self): diff --git a/pylibs/pylama/checkers/pylint/checkers/similar.py b/pylibs/pylama/checkers/pylint/checkers/similar.py index 40d8a123..f22116e9 100644 --- a/pylibs/pylama/checkers/pylint/checkers/similar.py +++ b/pylibs/pylama/checkers/pylint/checkers/similar.py @@ -1,5 +1,5 @@ # pylint: disable=W0622 -# Copyright (c) 2004-2012 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2004-2013 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -22,10 +22,10 @@ from ..logilab.common.ureports import Table from ..interfaces import IRawChecker -from ..checkers import BaseChecker, table_lines_from_stats +from . import BaseChecker, table_lines_from_stats -class Similar: +class Similar(object): """finds copy-pasted lines of code in a project""" def __init__(self, min_lines=4, ignore_comments=False, @@ -36,14 +36,21 @@ def __init__(self, min_lines=4, ignore_comments=False, self.ignore_imports = ignore_imports self.linesets = [] - def append_stream(self, streamid, stream): + def append_stream(self, streamid, stream, encoding=None): """append a file to search for similarities""" - stream.seek(0) # XXX may be removed with astng > 0.23 - self.linesets.append(LineSet(streamid, - stream.readlines(), - self.ignore_comments, - self.ignore_docstrings, - self.ignore_imports)) + stream.seek(0) # XXX may be removed with astroid > 0.23 + if encoding is None: + readlines = stream.readlines + else: + readlines = lambda: [line.decode(encoding) for line in stream] + try: + self.linesets.append(LineSet(streamid, + readlines(), + self.ignore_comments, + self.ignore_docstrings, + self.ignore_imports)) + except UnicodeDecodeError: + pass def run(self): """start looking for similarities and display results on stdout""" @@ -80,7 +87,7 @@ def _display_sims(self, sims): print "==%s:%s" % (lineset.name, idx) # pylint: disable=W0631 for line in lineset._real_lines[idx:idx+num]: - print " ", line, + print " ", line.rstrip() nb_lignes_dupliquees += num * (len(couples)-1) nb_total_lignes = sum([len(lineset) for lineset in self.linesets]) print "TOTAL lines=%s duplicates=%s percent=%.2f" \ @@ -153,7 +160,8 @@ def stripped_lines(lines, ignore_comments, ignore_docstrings, ignore_imports): strippedlines.append(line) return strippedlines -class LineSet: + +class LineSet(object): """Holds and indexes all the lines of a single source file""" def __init__(self, name, lines, ignore_comments=False, ignore_docstrings=False, ignore_imports=False): @@ -288,7 +296,7 @@ def process_module(self, node): stream must implement the readlines method """ - self.append_stream(self.linter.current_name, node.file_stream) + self.append_stream(self.linter.current_name, node.file_stream, node.file_encoding) def close(self): """compute and display similarities on closing (i.e. end of parsing)""" diff --git a/pylibs/pylama/checkers/pylint/checkers/stdlib.py b/pylibs/pylama/checkers/pylint/checkers/stdlib.py new file mode 100644 index 00000000..51450983 --- /dev/null +++ b/pylibs/pylama/checkers/pylint/checkers/stdlib.py @@ -0,0 +1,68 @@ +# Copyright 2012 Google Inc. +# +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +"""Checkers for various standard library functions.""" + +import re +import sys + +from .. import astroid + +from ..interfaces import IAstroidChecker +from . import BaseChecker, BaseTokenChecker, utils + +_VALID_OPEN_MODE_REGEX = r'^(r?U|[rwa]\+?b?)$' + +if sys.version_info >= (3, 0): + OPEN_MODULE = '_io' +else: + OPEN_MODULE = '__builtin__' + +class OpenModeChecker(BaseChecker): + __implements__ = (IAstroidChecker,) + name = 'open_mode' + + msgs = { + 'W1501': ('"%s" is not a valid mode for open.', + 'bad-open-mode', + 'Python supports: r, w, a modes with b, +, and U options. ' + 'See http://docs.python.org/2/library/functions.html#open'), + } + + @utils.check_messages('W1501') + def visit_callfunc(self, node): + """Visit a CallFunc node.""" + if hasattr(node, 'func'): + infer = utils.safe_infer(node.func) + if infer and infer.root().name == OPEN_MODULE: + if getattr(node.func, 'name', None) in ('open', 'file'): + self._check_open_mode(node) + + def _check_open_mode(self, node): + """Check that the mode argument of an open or file call is valid.""" + try: + mode_arg = utils.get_argument_from_call(node, position=1, keyword='mode') + if mode_arg: + mode_arg = utils.safe_infer(mode_arg) + if (isinstance(mode_arg, astroid.Const) + and not re.match(_VALID_OPEN_MODE_REGEX, mode_arg.value)): + self.add_message('W1501', node=node, args=(mode_arg.value)) + except (utils.NoSuchArgumentError, TypeError): + pass + +def register(linter): + """required method to auto register this checker """ + linter.register_checker(OpenModeChecker(linter)) + diff --git a/pylibs/pylama/checkers/pylint/checkers/strings.py b/pylibs/pylama/checkers/pylint/checkers/strings.py index 3c755dd7..3d5e8051 100644 --- a/pylibs/pylama/checkers/pylint/checkers/strings.py +++ b/pylibs/pylama/checkers/pylint/checkers/strings.py @@ -21,11 +21,10 @@ import sys import tokenize -from ..logilab import astng +from .. import astroid -from ..interfaces import IRawChecker, IASTNGChecker -from ..checkers import BaseChecker, BaseRawChecker -from ..checkers import utils +from ..interfaces import ITokenChecker, IAstroidChecker +from . import BaseChecker, BaseTokenChecker, utils _PY3K = sys.version_info >= (3, 0) @@ -72,26 +71,27 @@ specifiers is given too many arguments"), } -OTHER_NODES = (astng.Const, astng.List, astng.Backquote, - astng.Lambda, astng.Function, - astng.ListComp, astng.SetComp, astng.GenExpr) +OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, + astroid.Lambda, astroid.Function, + astroid.ListComp, astroid.SetComp, astroid.GenExpr) class StringFormatChecker(BaseChecker): """Checks string formatting operations to ensure that the format string is valid and the arguments match the format string. """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) name = 'string' msgs = MSGS + @utils.check_messages(*(MSGS.keys())) def visit_binop(self, node): if node.op != '%': return left = node.left args = node.right - if not (isinstance(left, astng.Const) + if not (isinstance(left, astroid.Const) and isinstance(left.value, basestring)): return format_string = left.value @@ -114,11 +114,11 @@ def visit_binop(self, node): # Check that the RHS of the % operator is a mapping object # that contains precisely the set of keys required by the # format string. - if isinstance(args, astng.Dict): + if isinstance(args, astroid.Dict): keys = set() unknown_keys = False for k, _ in args.items: - if isinstance(k, astng.Const): + if isinstance(k, astroid.Const): key = k.value if isinstance(key, basestring): keys.add(key) @@ -137,7 +137,7 @@ def visit_binop(self, node): for key in keys: if key not in required_keys: self.add_message('W1301', node=node, args=key) - elif isinstance(args, OTHER_NODES + (astng.Tuple,)): + elif isinstance(args, OTHER_NODES + (astroid.Tuple,)): type_name = type(args).__name__ self.add_message('E1303', node=node, args=type_name) # else: @@ -149,9 +149,9 @@ def visit_binop(self, node): # Check that the number of arguments passed to the RHS of # the % operator matches the number required by the format # string. - if isinstance(args, astng.Tuple): + if isinstance(args, astroid.Tuple): num_args = len(args.elts) - elif isinstance(args, OTHER_NODES + (astng.Dict, astng.DictComp)): + elif isinstance(args, OTHER_NODES + (astroid.Dict, astroid.DictComp)): num_args = 1 else: # The RHS of the format specifier is a name or @@ -166,7 +166,7 @@ def visit_binop(self, node): class StringMethodsChecker(BaseChecker): - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) name = 'string' msgs = { 'E1310': ("Suspicious argument in %s.%s call", @@ -175,24 +175,25 @@ class StringMethodsChecker(BaseChecker): " duplicate character, "), } + @utils.check_messages(*(MSGS.keys())) def visit_callfunc(self, node): func = utils.safe_infer(node.func) - if (isinstance(func, astng.BoundMethod) - and isinstance(func.bound, astng.Instance) + if (isinstance(func, astroid.BoundMethod) + and isinstance(func.bound, astroid.Instance) and func.bound.name in ('str', 'unicode', 'bytes') and func.name in ('strip', 'lstrip', 'rstrip') and node.args): arg = utils.safe_infer(node.args[0]) - if not isinstance(arg, astng.Const): + if not isinstance(arg, astroid.Const): return if len(arg.value) != len(set(arg.value)): self.add_message('E1310', node=node, args=(func.bound.name, func.name)) -class StringConstantChecker(BaseRawChecker): +class StringConstantChecker(BaseTokenChecker): """Check string literals""" - __implements__ = (IRawChecker, IASTNGChecker) + __implements__ = (ITokenChecker,) name = 'string_constant' msgs = { 'W1401': ('Anomalous backslash in string: \'%s\'. ' diff --git a/pylibs/pylama/checkers/pylint/checkers/typecheck.py b/pylibs/pylama/checkers/pylint/checkers/typecheck.py index 4cdc6006..e8938827 100644 --- a/pylibs/pylama/checkers/pylint/checkers/typecheck.py +++ b/pylibs/pylama/checkers/pylint/checkers/typecheck.py @@ -13,18 +13,18 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""try to find more bugs in the code using astng inference capabilities +"""try to find more bugs in the code using astroid inference capabilities """ import re import shlex -from ..logilab import astng -from ..logilab.astng import InferenceError, NotFoundError, YES, Instance +from .. import astroid +from ..astroid import InferenceError, NotFoundError, YES, Instance -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import safe_infer, is_super, check_messages +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import safe_infer, is_super, check_messages MSGS = { 'E1101': ('%s %r has no %r member', @@ -37,7 +37,7 @@ 'E1103': ('%s %r has no %r member (but some types could not be inferred)', 'maybe-no-member', 'Used when a variable is accessed for an unexistent member, but \ - astng was not able to interpret all possible types of this \ + astroid was not able to interpret all possible types of this \ variable.'), 'E1111': ('Assigning to function call which doesn\'t return', 'assignment-from-no-return', @@ -58,7 +58,8 @@ 'E1122': ('Duplicate keyword argument %r in function call', 'duplicate-keyword-arg', 'Used when a function call passes the same keyword argument \ - multiple times.'), + multiple times.', + {'maxversion': (2, 6)}), 'E1123': ('Passing unexpected keyword argument %r in function call', 'unexpected-keyword-arg', 'Used when a function call passes a keyword argument that \ @@ -68,13 +69,18 @@ 'Used when a function call would result in assigning multiple \ values to a function parameter, one value from a positional \ argument and one from a keyword argument.'), + 'E1125': ('Missing mandatory keyword argument %r', + 'missing-kwoa', + 'Used when a function call doesn\'t pass a mandatory \ + keyword-only argument.', + {'minversion': (3, 0)}), } class TypeChecker(BaseChecker): """try to find bugs in the code using type inference """ - __implements__ = (IASTNGChecker,) + __implements__ = (IAstroidChecker,) # configuration section name name = 'typecheck' @@ -120,7 +126,7 @@ def open(self): self.generated_members.extend(('REQUEST', 'acl_users', 'aq_parent')) def visit_assattr(self, node): - if isinstance(node.ass_type(), astng.AugAssign): + if isinstance(node.ass_type(), astroid.AugAssign): self.visit_getattr(node) def visit_delattr(self, node): @@ -162,7 +168,7 @@ def visit_getattr(self, node): inference_failure = True continue # skip None anyway - if isinstance(owner, astng.Const) and owner.value is None: + if isinstance(owner, astroid.Const) and owner.value is None: continue # XXX "super" / metaclass call if is_super(owner) or getattr(owner, 'type', None) == 'metaclass': @@ -174,14 +180,14 @@ def visit_getattr(self, node): continue try: if not [n for n in owner.getattr(node.attrname) - if not isinstance(n.statement(), astng.AugAssign)]: + if not isinstance(n.statement(), astroid.AugAssign)]: missingattr.add((owner, name)) continue except AttributeError: # XXX method / function continue except NotFoundError: - if isinstance(owner, astng.Function) and owner.decorators: + if isinstance(owner, astroid.Function) and owner.decorators: continue if isinstance(owner, Instance) and owner.has_dynamic_getattr(): continue @@ -212,33 +218,34 @@ def visit_getattr(self, node): args=(owner.display_type(), name, node.attrname)) - + @check_messages('E1111', 'W1111') def visit_assign(self, node): """check that if assigning to a function call, the function is possibly returning something valuable """ - if not isinstance(node.value, astng.CallFunc): + if not isinstance(node.value, astroid.CallFunc): return function_node = safe_infer(node.value.func) # skip class, generator and incomplete function definition - if not (isinstance(function_node, astng.Function) and + if not (isinstance(function_node, astroid.Function) and function_node.root().fully_defined()): return if function_node.is_generator() \ or function_node.is_abstract(pass_is_abstract=False): return - returns = list(function_node.nodes_of_class(astng.Return, - skip_klass=astng.Function)) + returns = list(function_node.nodes_of_class(astroid.Return, + skip_klass=astroid.Function)) if len(returns) == 0: self.add_message('E1111', node=node) else: for rnode in returns: - if not (isinstance(rnode.value, astng.Const) + if not (isinstance(rnode.value, astroid.Const) and rnode.value.value is None): break else: self.add_message('W1111', node=node) + @check_messages(*(MSGS.keys())) def visit_callfunc(self, node): """check that called functions/methods are inferred to callable objects, and that the arguments passed to the function match the parameters in @@ -250,7 +257,7 @@ def visit_callfunc(self, node): keyword_args = set() num_positional_args = 0 for arg in node.args: - if isinstance(arg, astng.Keyword): + if isinstance(arg, astroid.Keyword): keyword = arg.arg if keyword in keyword_args: self.add_message('E1122', node=node, args=keyword) @@ -265,18 +272,18 @@ def visit_callfunc(self, node): # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must # come first in this 'if..else'. - if isinstance(called, astng.BoundMethod): + if isinstance(called, astroid.BoundMethod): # Bound methods have an extra implicit 'self' argument. num_positional_args += 1 - elif isinstance(called, astng.UnboundMethod): + elif isinstance(called, astroid.UnboundMethod): if called.decorators is not None: for d in called.decorators.nodes: - if isinstance(d, astng.Name) and (d.name == 'classmethod'): + if isinstance(d, astroid.Name) and (d.name == 'classmethod'): # Class methods have an extra implicit 'cls' argument. num_positional_args += 1 break - elif (isinstance(called, astng.Function) or - isinstance(called, astng.Lambda)): + elif (isinstance(called, astroid.Function) or + isinstance(called, astroid.Lambda)): pass else: return @@ -295,15 +302,15 @@ def visit_callfunc(self, node): parameters = [] parameter_name_to_index = {} for i, arg in enumerate(called.args.args): - if isinstance(arg, astng.Tuple): + if isinstance(arg, astroid.Tuple): name = None # Don't store any parameter names within the tuple, since those # are not assignable from keyword arguments. else: - if isinstance(arg, astng.Keyword): + if isinstance(arg, astroid.Keyword): name = arg.arg else: - assert isinstance(arg, astng.AssName) + assert isinstance(arg, astroid.AssName) # This occurs with: # def f( (a), (b) ): pass name = arg.name @@ -314,6 +321,15 @@ def visit_callfunc(self, node): defval = None parameters.append([(name, defval), False]) + kwparams = {} + for i, arg in enumerate(called.args.kwonlyargs): + if isinstance(arg, astroid.Keyword): + name = arg.arg + else: + assert isinstance(arg, astroid.AssName) + name = arg.name + kwparams[name] = [called.args.kw_defaults[i], False] + # Match the supplied arguments against the function parameters. # 1. Match the positional arguments. @@ -338,6 +354,12 @@ def visit_callfunc(self, node): self.add_message('E1124', node=node, args=keyword) else: parameters[i][1] = True + elif keyword in kwparams: + if kwparams[keyword][1]: # XXX is that even possible? + # Duplicate definition of function parameter. + self.add_message('E1124', node=node, args=keyword) + else: + kwparams[keyword][1] = True elif called.args.kwarg is not None: # The keyword argument gets assigned to the **kwargs parameter. pass @@ -382,6 +404,12 @@ def visit_callfunc(self, node): display_name = repr(name) self.add_message('E1120', node=node, args=display_name) + for name in kwparams: + defval, assigned = kwparams[name] + if defval is None and not assigned: + self.add_message('E1125', node=node, args=name) + + def register(linter): """required method to auto register this checker """ linter.register_checker(TypeChecker(linter)) diff --git a/pylibs/pylama/checkers/pylint/checkers/utils.py b/pylibs/pylama/checkers/pylint/checkers/utils.py index 34d335bb..1a7dca9e 100644 --- a/pylibs/pylama/checkers/pylint/checkers/utils.py +++ b/pylibs/pylama/checkers/pylint/checkers/utils.py @@ -21,19 +21,23 @@ import re import string -from ..logilab import astng -from ..logilab.astng import scoped_nodes +from .. import astroid + +from ..astroid import scoped_nodes from ..logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ -COMP_NODE_TYPES = astng.ListComp, astng.SetComp, astng.DictComp, astng.GenExpr +COMP_NODE_TYPES = astroid.ListComp, astroid.SetComp, astroid.DictComp, astroid.GenExpr + +class NoSuchArgumentError(Exception): + pass def is_inside_except(node): """Returns true if node is inside the name of an except handler.""" current = node - while current and not isinstance(current.parent, astng.ExceptHandler): + while current and not isinstance(current.parent, astroid.ExceptHandler): current = current.parent return current and current is current.parent.name @@ -41,7 +45,7 @@ def is_inside_except(node): def get_all_elements(node): """Recursively returns all atoms in nested lists and tuples.""" - if isinstance(node, (astng.Tuple, astng.List)): + if isinstance(node, (astroid.Tuple, astroid.List)): for child in node.elts: for e in get_all_elements(child): yield e @@ -56,9 +60,9 @@ def clobber_in_except(node): Returns (True, args for W0623) if assignment clobbers an existing variable, (False, None) otherwise. """ - if isinstance(node, astng.AssAttr): + if isinstance(node, astroid.AssAttr): return (True, (node.attrname, 'object %r' % (node.expr.name,))) - elif isinstance(node, astng.AssName): + elif isinstance(node, astroid.AssName): name = node.name if is_builtin(name): return (True, (name, 'builtins')) @@ -66,7 +70,7 @@ def clobber_in_except(node): scope, stmts = node.lookup(name) if (stmts and not isinstance(stmts[0].ass_type(), - (astng.Assign, astng.AugAssign, astng.ExceptHandler))): + (astroid.Assign, astroid.AugAssign, astroid.ExceptHandler))): return (True, (name, 'outer scope (line %s)' % (stmts[0].fromlineno,))) return (False, None) @@ -79,12 +83,12 @@ def safe_infer(node): try: inferit = node.infer() value = inferit.next() - except astng.InferenceError: + except astroid.InferenceError: return try: inferit.next() return # None if there is ambiguity on the inferred node - except astng.InferenceError: + except astroid.InferenceError: return # there is some kind of ambiguity except StopIteration: return value @@ -100,24 +104,28 @@ def is_super(node): def is_error(node): """return true if the function does nothing but raising an exception""" for child_node in node.get_children(): - if isinstance(child_node, astng.Raise): + if isinstance(child_node, astroid.Raise): return True return False def is_raising(body): """return true if the given statement node raise an exception""" for node in body: - if isinstance(node, astng.Raise): + if isinstance(node, astroid.Raise): return True return False def is_empty(body): """return true if the given node does nothing but 'pass'""" - return len(body) == 1 and isinstance(body[0], astng.Pass) + return len(body) == 1 and isinstance(body[0], astroid.Pass) builtins = builtins.__dict__.copy() SPECIAL_BUILTINS = ('__builtins__',) # '__path__', '__file__') +def is_builtin_object(node): + """Returns True if the given node is an object from the __builtin__ module.""" + return node and node.root().name == '__builtin__' + def is_builtin(name): # was is_native_builtin """return true if could be considered as a builtin defined by python """ @@ -136,20 +144,20 @@ def is_defined_before(var_node): _node = var_node.parent while _node: if isinstance(_node, COMP_NODE_TYPES): - for ass_node in _node.nodes_of_class(astng.AssName): + for ass_node in _node.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - elif isinstance(_node, astng.For): - for ass_node in _node.target.nodes_of_class(astng.AssName): + elif isinstance(_node, astroid.For): + for ass_node in _node.target.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - elif isinstance(_node, astng.With): - if _node.vars is None: - # quickfix : case in which 'with' is used without 'as' - return False - if _node.vars.name == varname: - return True - elif isinstance(_node, (astng.Lambda, astng.Function)): + elif isinstance(_node, astroid.With): + for expr, vars in _node.items: + if expr.parent_of(var_node): + break + if vars and vars.name == varname: + return True + elif isinstance(_node, (astroid.Lambda, astroid.Function)): if _node.args.is_argument(varname): return True if getattr(_node, 'name', None) == varname: @@ -161,10 +169,10 @@ def is_defined_before(var_node): _node = stmt.previous_sibling() lineno = stmt.fromlineno while _node and _node.fromlineno == lineno: - for ass_node in _node.nodes_of_class(astng.AssName): + for ass_node in _node.nodes_of_class(astroid.AssName): if ass_node.name == varname: return True - for imp_node in _node.nodes_of_class( (astng.From, astng.Import)): + for imp_node in _node.nodes_of_class( (astroid.From, astroid.Import)): if varname in [name[1] or name[0] for name in imp_node.names]: return True _node = _node.previous_sibling() @@ -175,9 +183,9 @@ def is_func_default(node): value """ parent = node.scope() - if isinstance(parent, astng.Function): + if isinstance(parent, astroid.Function): for default_node in parent.args.defaults: - for default_name_node in default_node.nodes_of_class(astng.Name): + for default_name_node in default_node.nodes_of_class(astroid.Name): if default_name_node is node: return True return False @@ -186,10 +194,10 @@ def is_func_decorator(node): """return true if the name is used in function decorator""" parent = node.parent while parent is not None: - if isinstance(parent, astng.Decorators): + if isinstance(parent, astroid.Decorators): return True if (parent.is_statement or - isinstance(parent, astng.Lambda) or + isinstance(parent, astroid.Lambda) or isinstance(parent, (scoped_nodes.ComprehensionScope, scoped_nodes.ListComp))): break @@ -197,7 +205,7 @@ def is_func_decorator(node): return False def is_ancestor_name(frame, node): - """return True if `frame` is a astng.Class node with `node` in the + """return True if `frame` is a astroid.Class node with `node` in the subtree of its bases attribute """ try: @@ -205,23 +213,23 @@ def is_ancestor_name(frame, node): except AttributeError: return False for base in bases: - if node in base.nodes_of_class(astng.Name): + if node in base.nodes_of_class(astroid.Name): return True return False def assign_parent(node): """return the higher parent which is not an AssName, Tuple or List node """ - while node and isinstance(node, (astng.AssName, - astng.Tuple, - astng.List)): + while node and isinstance(node, (astroid.AssName, + astroid.Tuple, + astroid.List)): node = node.parent return node def overrides_an_abstract_method(class_node, name): """return True if pnode is a parent of node""" for ancestor in class_node.ancestors(): - if name in ancestor and isinstance(ancestor[name], astng.Function) and \ + if name in ancestor and isinstance(ancestor[name], astroid.Function) and \ ancestor[name].is_abstract(pass_is_abstract=False): return True return False @@ -229,7 +237,7 @@ def overrides_an_abstract_method(class_node, name): def overrides_a_method(class_node, name): """return True if is a method overridden from an ancestor""" for ancestor in class_node.ancestors(): - if name in ancestor and isinstance(ancestor[name], astng.Function): + if name in ancestor and isinstance(ancestor[name], astroid.Function): return True return False @@ -352,7 +360,7 @@ def node_frame_class(node): """ klass = node.frame() - while klass is not None and not isinstance(klass, astng.Class): + while klass is not None and not isinstance(klass, astroid.Class): if klass.parent is None: klass = None else: @@ -364,8 +372,8 @@ def is_super_call(expr): """return True if expression node is a function call and if function name is super. Check before that you're in a method. """ - return (isinstance(expr, astng.CallFunc) and - isinstance(expr.func, astng.Name) and + return (isinstance(expr, astroid.CallFunc) and + isinstance(expr.func, astroid.Name) and expr.func.name == 'super') def is_attr_private(attrname): @@ -374,3 +382,28 @@ def is_attr_private(attrname): """ regex = re.compile('^_{2,}.*[^_]+_?$') return regex.match(attrname) + +def get_argument_from_call(callfunc_node, position=None, keyword=None): + """Returns the specified argument from a function call. + + :param callfunc_node: Node representing a function call to check. + :param int position: position of the argument. + :param str keyword: the keyword of the argument. + + :returns: The node representing the argument, None if the argument is not found. + :raises ValueError: if both position and keyword are None. + :raises NoSuchArgumentError: if no argument at the provided position or with + the provided keyword. + """ + if not position and not keyword: + raise ValueError('Must specify at least one of: position or keyword.') + try: + if position and not isinstance(callfunc_node.args[position], astroid.Keyword): + return callfunc_node.args[position] + except IndexError as error: + raise NoSuchArgumentError(error) + if keyword: + for arg in callfunc_node.args: + if isinstance(arg, astroid.Keyword) and arg.arg == keyword: + return arg.value + raise NoSuchArgumentError diff --git a/pylibs/pylama/checkers/pylint/checkers/variables.py b/pylibs/pylama/checkers/pylint/checkers/variables.py index 88bd2a51..afe0f945 100644 --- a/pylibs/pylama/checkers/pylint/checkers/variables.py +++ b/pylibs/pylama/checkers/pylint/checkers/variables.py @@ -19,12 +19,12 @@ import sys from copy import copy -from ..logilab import astng -from ..logilab.astng import are_exclusive, builtin_lookup, ASTNGBuildingException +from .. import astroid +from ..astroid import are_exclusive, builtin_lookup, AstroidBuildingException -from ..interfaces import IASTNGChecker -from ..checkers import BaseChecker -from ..checkers.utils import (PYMETHODS, is_ancestor_name, is_builtin, +from ..interfaces import IAstroidChecker +from . import BaseChecker +from .utils import (PYMETHODS, is_ancestor_name, is_builtin, is_defined_before, is_error, is_func_default, is_func_decorator, assign_parent, check_messages, is_inside_except, clobber_in_except, get_all_elements) @@ -32,7 +32,7 @@ def in_for_else_branch(parent, stmt): """Returns True if stmt in inside the else branch for a parent For stmt.""" - return (isinstance(parent, astng.For) and + return (isinstance(parent, astroid.For) and any(else_stmt.parent_of(stmt) for else_stmt in parent.orelse)) def overridden_method(klass, name): @@ -45,9 +45,9 @@ def overridden_method(klass, name): meth_node = parent[name] except KeyError: # We have found an ancestor defining but it's not in the local - # dictionary. This may happen with astng built from living objects. + # dictionary. This may happen with astroid built from living objects. return None - if isinstance(meth_node, astng.Function): + if isinstance(meth_node, astroid.Function): return meth_node return None @@ -118,6 +118,12 @@ def overridden_method(klass, name): 'Used when an loop variable (i.e. defined by a for loop or \ a list comprehension or a generator expression) is used outside \ the loop.'), + + 'W0632': ('Possible unbalanced tuple unpacking: ' + 'left side has %d label(s), right side has %d value(s)', + 'unbalanced-tuple-unpacking', + 'Used when there is an unbalanced tuple unpacking in assignment'), + } class VariablesChecker(BaseChecker): @@ -129,7 +135,7 @@ class VariablesChecker(BaseChecker): * __all__ consistency """ - __implements__ = IASTNGChecker + __implements__ = IAstroidChecker name = 'variables' msgs = MSGS @@ -140,7 +146,7 @@ class VariablesChecker(BaseChecker): 'help' : 'Tells whether we should check for unused import in \ __init__ files.'}), ("dummy-variables-rgx", - {'default': ('_|dummy'), + {'default': ('_$|dummy'), 'type' :'regexp', 'metavar' : '', 'help' : 'A regular expression matching the beginning of \ the name of dummy variables (i.e. not used).'}), @@ -166,7 +172,7 @@ def visit_module(self, node): # do not print Redefining builtin for additional builtins self.add_message('W0622', args=name, node=stmts[0]) - @check_messages('W0611', 'W0614') + @check_messages('W0611', 'W0614', 'W0622', 'E0603', 'E0604') def leave_module(self, node): """leave module: check globals """ @@ -175,14 +181,14 @@ def leave_module(self, node): # attempt to check for __all__ if defined if '__all__' in node.locals: assigned = node.igetattr('__all__').next() - if assigned is not astng.YES: + if assigned is not astroid.YES: for elt in getattr(assigned, 'elts', ()): try: elt_name = elt.infer().next() - except astng.InferenceError: + except astroid.InferenceError: continue - if not isinstance(elt_name, astng.Const) or not isinstance(elt_name.value, basestring): + if not isinstance(elt_name, astroid.Const) or not isinstance(elt_name.value, basestring): self.add_message('E0604', args=elt.as_string(), node=elt) continue elt_name = elt.value @@ -197,9 +203,9 @@ def leave_module(self, node): return for name, stmts in not_consumed.iteritems(): stmt = stmts[0] - if isinstance(stmt, astng.Import): + if isinstance(stmt, astroid.Import): self.add_message('W0611', args=name, node=stmt) - elif isinstance(stmt, astng.From) and stmt.modname != '__future__': + elif isinstance(stmt, astroid.From) and stmt.modname != '__future__': if stmt.names[0][0] == '*': self.add_message('W0614', args=name, node=stmt) else: @@ -271,9 +277,11 @@ def visit_function(self, node): for name, stmt in node.items(): if is_inside_except(stmt): continue - if name in globs and not isinstance(stmt, astng.Global): + if name in globs and not isinstance(stmt, astroid.Global): line = globs[name][0].fromlineno - self.add_message('W0621', args=(name, line), node=stmt) + dummy_rgx = self.config.dummy_variables_rgx + if not dummy_rgx.match(name): + self.add_message('W0621', args=(name, line), node=stmt) elif is_builtin(name): # do not print Redefining builtin for additional builtins self.add_message('W0622', args=name, node=stmt) @@ -301,7 +309,7 @@ def leave_function(self, node): # ignore names imported by the global statement # FIXME: should only ignore them if it's assigned latter stmt = stmts[0] - if isinstance(stmt, astng.Global): + if isinstance(stmt, astroid.Global): continue # care about functions with unknown argument (builtins) if name in argnames: @@ -328,7 +336,7 @@ def leave_function(self, node): def visit_global(self, node): """check names imported exists in the global scope""" frame = node.frame() - if isinstance(frame, astng.Module): + if isinstance(frame, astroid.Module): self.add_message('W0604', node=node) return module = frame.root() @@ -336,7 +344,7 @@ def visit_global(self, node): for name in node.names: try: assign_nodes = module.getattr(name) - except astng.NotFoundError: + except astroid.NotFoundError: # unassigned global, skip assign_nodes = [] for anode in assign_nodes: @@ -399,10 +407,11 @@ def _loopvar_name(self, node, name): astmts = _astmts if len(astmts) == 1: ass = astmts[0].ass_type() - if isinstance(ass, (astng.For, astng.Comprehension, astng.GenExpr)) \ + if isinstance(ass, (astroid.For, astroid.Comprehension, astroid.GenExpr)) \ and not ass.statement() is node.statement(): self.add_message('W0631', args=name, node=node) + @check_messages('W0623') def visit_excepthandler(self, node): for name in get_all_elements(node.name): clobbering, args = clobber_in_except(name) @@ -410,19 +419,20 @@ def visit_excepthandler(self, node): self.add_message('W0623', args=args, node=name) def visit_assname(self, node): - if isinstance(node.ass_type(), astng.AugAssign): + if isinstance(node.ass_type(), astroid.AugAssign): self.visit_name(node) def visit_delname(self, node): self.visit_name(node) + @check_messages(*(MSGS.keys())) def visit_name(self, node): """check that a name is defined if the current scope and doesn't redefine a built-in """ stmt = node.statement() if stmt.fromlineno is None: - # name node from a astng built from live code, skip + # name node from a astroid built from live code, skip assert not stmt.root().file.endswith('.py') return name = node.name @@ -481,13 +491,13 @@ def visit_name(self, node): and stmt.fromlineno <= defstmt.fromlineno and not is_defined_before(node) and not are_exclusive(stmt, defstmt, ('NameError', 'Exception', 'BaseException'))): - if defstmt is stmt and isinstance(node, (astng.DelName, - astng.AssName)): + if defstmt is stmt and isinstance(node, (astroid.DelName, + astroid.AssName)): self.add_message('E0602', args=name, node=node) elif self._to_consume[-1][-1] != 'lambda': # E0601 may *not* occurs in lambda scope self.add_message('E0601', args=name, node=node) - if not isinstance(node, astng.AssName): # Aug AssName + if not isinstance(node, astroid.AssName): # Aug AssName del to_consume[name] else: del consumed[name] @@ -497,7 +507,7 @@ def visit_name(self, node): else: # we have not found the name, if it isn't a builtin, that's an # undefined name ! - if not (name in astng.Module.scope_attrs or is_builtin(name) + if not (name in astroid.Module.scope_attrs or is_builtin(name) or name in self.config.additional_builtins): self.add_message('E0602', args=name, node=node) @@ -508,7 +518,7 @@ def visit_import(self, node): parts = name.split('.') try: module = node.infer_name_module(parts[0]).next() - except astng.ResolveError: + except astroid.ResolveError: continue self._check_module_attrs(node, module, parts[1:]) @@ -519,7 +529,7 @@ def visit_from(self, node): level = getattr(node, 'level', None) try: module = node.root().import_module(name_parts[0], level=level) - except ASTNGBuildingException: + except AstroidBuildingException: return except Exception, exc: print 'Unhandled exception in VariablesChecker:', exc @@ -532,12 +542,33 @@ def visit_from(self, node): continue self._check_module_attrs(node, module, name.split('.')) + @check_messages('unbalanced-tuple-unpacking') + def visit_assign(self, node): + """Check unbalanced tuple unpacking for assignments""" + if not isinstance(node.targets[0], (astroid.Tuple, astroid.List)): + return + try: + infered = node.value.infer().next() + except astroid.InferenceError: + return + if not isinstance(infered, (astroid.Tuple, astroid.List)): + return + targets = node.targets[0].itered() + values = infered.itered() + if any(not isinstance(target_node, astroid.AssName) + for target_node in targets): + return + if len(targets) != len(values): + self.add_message('unbalanced-tuple-unpacking', + node=node, + args=(len(targets), len(values))) + def _check_module_attrs(self, node, module, module_names): """check that module_names (list of string) are accessible through the given module if the latest access name corresponds to a module, return it """ - assert isinstance(module, astng.Module), module + assert isinstance(module, astroid.Module), module while module_names: name = module_names.pop(0) if name == '__dict__': @@ -545,12 +576,12 @@ def _check_module_attrs(self, node, module, module_names): break try: module = module.getattr(name)[0].infer().next() - if module is astng.YES: + if module is astroid.YES: return None - except astng.NotFoundError: + except astroid.NotFoundError: self.add_message('E0611', args=(name, module.name), node=node) return None - except astng.InferenceError: + except astroid.InferenceError: return None if module_names: # FIXME: other message if name is not the latest part of @@ -559,7 +590,7 @@ def _check_module_attrs(self, node, module, module_names): self.add_message('E0611', node=node, args=('.'.join(module_names), modname)) return None - if isinstance(module, astng.Module): + if isinstance(module, astroid.Module): return module return None diff --git a/pylibs/pylama/checkers/pylint/config.py b/pylibs/pylama/checkers/pylint/config.py index 94d44b8f..192f2548 100644 --- a/pylibs/pylama/checkers/pylint/config.py +++ b/pylibs/pylama/checkers/pylint/config.py @@ -11,7 +11,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -"""utilities for PyLint configuration : +"""utilities for Pylint configuration : * pylintrc * pylint.d (PYLINTHOME) @@ -105,13 +105,13 @@ def find_pylintrc(): PYLINTRC = find_pylintrc() ENV_HELP = ''' -The following environment variables are used: - * PYLINTHOME - path to the directory where data of persistent run will be stored. If not -found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working -directory). - * PYLINTRC - path to the configuration file. If not found, it will use the first +The following environment variables are used: + * PYLINTHOME + path to the directory where data of persistent run will be stored. If not +found, it defaults to ~/.pylint.d/ or .pylint.d (in the current working +directory). + * PYLINTRC + path to the configuration file. If not found, it will use the first existing file among (~/.pylintrc, /etc/pylintrc). ''' % globals() diff --git a/pylibs/pylama/checkers/pylint/interfaces.py b/pylibs/pylama/checkers/pylint/interfaces.py index 2d2432df..7193c65a 100644 --- a/pylibs/pylama/checkers/pylint/interfaces.py +++ b/pylibs/pylama/checkers/pylint/interfaces.py @@ -10,13 +10,7 @@ # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" Copyright (c) 2002-2003 LOGILAB S.A. (Paris, FRANCE). - http://www.logilab.fr/ -- mailto:contact@logilab.fr - -Interfaces for PyLint objects -""" - -__revision__ = "$Id: interfaces.py,v 1.9 2004-04-24 12:14:53 syt Exp $" +"""Interfaces for PyLint objects""" from .logilab.common.interface import Interface @@ -32,51 +26,31 @@ def open(self): def close(self): """called after visiting project (i.e set of modules)""" -## def open_module(self): -## """called before visiting a module""" - -## def close_module(self): -## """called after visiting a module""" - class IRawChecker(IChecker): """interface for checker which need to parse the raw file """ - def process_module(self, astng): + def process_module(self, astroid): """ process a module - the module's content is accessible via astng.file_stream + the module's content is accessible via astroid.file_stream """ -class IASTNGChecker(IChecker): - """ interface for checker which prefers receive events according to - statement type - """ - - -class ILinter(Interface): - """interface for the linter class +class ITokenChecker(IChecker): + """Interface for checkers that need access to the token list.""" + def process_tokens(self, tokens): + """Process a module. - the linter class will generate events to its registered checkers. - Each checker may interact with the linter instance using this API - """ - - def register_checker(self, checker): - """register a new checker class - - checker is a class implementing IrawChecker or / and IASTNGChecker + tokens is a list of all source code tokens in the file. """ - def add_message(self, msg_id, line=None, node=None, args=None): - """add the message corresponding to the given id. - - If provided, msg is expanded using args - astng checkers should provide the node argument, - raw checkers should provide the line argument. - """ +class IAstroidChecker(IChecker): + """ interface for checker which prefers receive events according to + statement type + """ class IReporter(Interface): @@ -95,4 +69,4 @@ def display_results(self, layout): """ -__all__ = ('IRawChecker', 'ILinter', 'IReporter') +__all__ = ('IRawChecker', 'IAstroidChecker', 'ITokenChecker', 'IReporter') diff --git a/pylibs/pylama/checkers/pylint/lint.py b/pylibs/pylama/checkers/pylint/lint.py index 60a9784b..25c5377b 100644 --- a/pylibs/pylama/checkers/pylint/lint.py +++ b/pylibs/pylama/checkers/pylint/lint.py @@ -31,7 +31,6 @@ import sys import os -import re import tokenize from warnings import warn @@ -43,30 +42,24 @@ from .logilab.common.ureports import Table, Text, Section from .logilab.common.__pkginfo__ import version as common_version -from .logilab.astng import MANAGER, nodes, ASTNGBuildingException -from .logilab.astng.__pkginfo__ import version as astng_version - -from .utils import (PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, - ReportsHandlerMixIn, MSG_TYPES, expand_modules, - WarningScope) -from .interfaces import ILinter, IRawChecker, IASTNGChecker -from .checkers import (BaseRawChecker, EmptyReport, - table_lines_from_stats) -from .reporters.text import (TextReporter, ParseableTextReporter, - VSTextReporter, ColorizedTextReporter) -from .reporters.html import HTMLReporter +from .astroid import MANAGER, nodes, AstroidBuildingException +from .astroid.__pkginfo__ import version as astroid_version + +from .utils import ( + MSG_TYPES, OPTION_RGX, + PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, + EmptyReport, WarningScope, + expand_modules, tokenize_module) +from .interfaces import IRawChecker, ITokenChecker, IAstroidChecker +from .checkers import (BaseTokenChecker, + table_lines_from_stats, + initialize as checkers_initialize) +from .reporters import initialize as reporters_initialize from . import config from .__pkginfo__ import version -OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)') -REPORTER_OPT_MAP = {'text': TextReporter, - 'parseable': ParseableTextReporter, - 'msvs': VSTextReporter, - 'colorized': ColorizedTextReporter, - 'html': HTMLReporter,} - def _get_python_path(filepath): dirname = os.path.dirname(os.path.realpath( @@ -88,8 +81,8 @@ def _get_python_path(filepath): 'Used when an error occurred preventing the analysis of a \ module (unable to find it for instance).'), 'F0002': ('%s: %s', - 'astng-error', - 'Used when an unexpected error occurred while building the ASTNG \ + 'astroid-error', + 'Used when an unexpected error occurred while building the Astroid \ representation. This is usually accompanied by a traceback. \ Please report such errors !'), 'F0003': ('ignored builtin module %s', @@ -102,8 +95,8 @@ def _get_python_path(filepath): inferred.'), 'F0010': ('error while code parsing: %s', 'parse-error', - 'Used when an exception occured while building the ASTNG \ - representation which could be handled by astng.'), + 'Used when an exception occured while building the Astroid \ + representation which could be handled by astroid.'), 'I0001': ('Unable to run raw checkers on built-in module %s', 'raw-checker-failed', @@ -157,21 +150,21 @@ def _get_python_path(filepath): class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, - BaseRawChecker): + BaseTokenChecker): """lint Python modules using external checkers. This is the main checker controlling the other ones and the reports - generation. It is itself both a raw checker and an astng checker in order + generation. It is itself both a raw checker and an astroid checker in order to: * handle message activation / deactivation at the module level * handle some basic but necessary stats'data (number of classes, methods...) IDE plugins developpers: you may have to call - `logilab.astng.builder.MANAGER.astng_cache.clear()` accross run if you want + `astroid.builder.MANAGER.astroid_cache.clear()` accross run if you want to ensure the latest code version is actually checked. """ - __implements__ = (ILinter, IRawChecker) + __implements__ = (ITokenChecker,) name = 'master' priority = 0 @@ -206,18 +199,6 @@ def make_options(): 'can also give a reporter class, eg mypackage.mymodule.' 'MyReporterClass.'}), - ('include-ids', - {'type' : 'yn', 'metavar' : '', 'default' : 0, - 'short': 'i', - 'group': 'Reports', - 'help' : 'Include message\'s id in output'}), - - ('symbols', - {'type' : 'yn', 'metavar' : '', 'default' : 0, - 'short': 's', - 'group': 'Reports', - 'help' : 'Include symbolic ids of messages in output'}), - ('files-output', {'default': 0, 'type' : 'yn', 'metavar' : '', 'group': 'Reports', 'level': 1, @@ -274,6 +255,16 @@ def make_options(): 'If you want to run only the classes checker, but have no ' 'Warning level messages displayed, use' '"--disable=all --enable=classes --disable=W"'}), + + ('msg-template', + {'type' : 'string', 'metavar': '