diff options
author | Sylvain Th?nault <thenault@gmail.com> | 2014-04-11 09:41:27 +0200 |
---|---|---|
committer | Sylvain Th?nault <thenault@gmail.com> | 2014-04-11 09:41:27 +0200 |
commit | bdad946a1894a740956ef74d63dc7a8ca491d497 (patch) | |
tree | 0f756079a5ff333da32d81bc267a6d22f3e80574 | |
parent | 63e524fe67ef9391dc37e5f1034c6d493e3ec008 (diff) | |
parent | c674b94c40d4069cc269acae75dd3636a76765ee (diff) | |
download | pylint-bdad946a1894a740956ef74d63dc7a8ca491d497.tar.gz |
Merged in dnozay/pylint (pull request #87)
fix error with message reports when custom checker uses old-style messages
104 files changed, 1558 insertions, 410 deletions
@@ -9,3 +9,4 @@ ^dist/ ^pylint.egg-info/ .tox +(^|/)\..*\.sw[a-z]$ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 8e5d5b4..8b28ae3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -5,8 +5,12 @@ Order doesn't matter (not that much, at least ;) * Sylvain Thenault (Logilab): main author / maintainer -* Torsten Marek (Google): maintainer, main GPyLint developper and (GooglePylint) - upstream integrator +* Torsten Marek (Google): maintainer, contributor + +* Claudiu Popa: maintainer, contributor + +* Daniel Balparda (Google): GPyLint maintainer (Google's pylint variant), + various patches * Martin Pool (Google): warnings for anomalous backslashes, symbolic names for messages (like 'unused'), etc @@ -17,7 +21,8 @@ Order doesn't matter (not that much, at least ;) * Sandro Tosi: Debian packaging -* Claudiu Popa, Mads Kiilerich, Boris Feld: various patches +* Mads Kiilerich, Boris Feld, Bill Wendling, Sebastian Ulrich: + various patches * Brian van den Broek: windows installation documentation @@ -2,6 +2,38 @@ ChangeLog for Pylint ==================== -- + + * Do not crash with UnknownMessage if an unknown message ID/name appears + in disable or enable in the configuration. Patch by Cole Robinson. + Fixes bitbucket issue #170. + + * Add new warning 'eval-used', checking that the builtin function `eval` + was used. + + * Make it possible to show a naming hint for invalid name by setting + include-naming-hint. Also make the naming hints configurable. Fixes + BitBucket issue #138. + + * Added support for enforcing multiple, but consistent name styles for + different name types inside a single module; based on a patch written + by morbo@google.com. + + * Also warn about empty docstrings on overridden methods; contributed + by sebastianu@google.com. + + * Also inspect arguments to constructor calls, and emit relevant + warnings; contributed by sebastianu@google.com. + + * Added a new configuration option logging-modules to make the list + of module names that can be checked for 'logging-not-lazy' et. al. + configurable; contributed by morbo@google.com. + + * ensure init-hooks is evaluated before other options, notably load-plugins + (#166) + + * Python 2.5 support restored: fixed small issues preventing pylint to run + on python 2.5. Bitbucket issues #50 and #62. + * bitbucket #128: pylint doesn't crash when looking for used-before-assignment in context manager assignments. @@ -17,6 +49,23 @@ ChangeLog for Pylint that `raise ... from ...` uses a proper exception context (None or an exception). + * Enhance the check for 'used-before-assignment' to look + for 'nonlocal' uses. + + * Emit 'undefined-all-variable' if a package's __all__ + variable contains a missing submodule (closes #126). + + * Add a new warning 'abstract-class-instantiated' for checking + that abstract classes created with `abc` module and + with abstract methods are instantied. + + * Do not warn about 'return-arg-in-generator' in Python 3.3+. + + * Do not warn about 'abstract-method' when the abstract method + is implemented through assignment (#155). + + * Add new warnings for checking proper class __slots__: + 'invalid-slots-object' and 'invalid-slots'. 2013-12-22 -- 1.1.0 * Add new check for use of deprecated pragma directives "pylint:disable-msg" @@ -1,3 +1,3 @@ python-logilab-common (>= 0.19.0) -python-astroid +python-astroid (>= 1.0.1) python-tk diff --git a/__init__.py b/__init__.py index dfb4386..eed1b62 100644 --- a/__init__.py +++ b/__init__.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys def run_pylint(): diff --git a/__pkginfo__.py b/__pkginfo__.py index eff50aa..b48272d 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -1,5 +1,5 @@ # pylint: disable=W0622,C0103 -# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2014 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,15 +13,20 @@ # # 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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """pylint packaging information""" +import sys modname = distname = 'pylint' numversion = (1, 1, 0) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common >= 0.53.0', 'astroid >= 0.24.3'] +if sys.version_info < (2, 6): + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.0.1', + 'StringFormat'] +else: + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.0.1'] license = 'GPL' description = "python code static checker" diff --git a/checkers/__init__.py b/checkers/__init__.py index 1d0aa42..9346904 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """utilities methods and classes for checkers Base id of standard checkers (used in msg and report ids): @@ -41,7 +41,6 @@ messages nor reports. XXX not true, emit a 07 report ! import sys import tokenize import warnings -from os.path import dirname from astroid.utils import ASTWalker from logilab.common.configuration import OptionsProviderMixIn diff --git a/checkers/base.py b/checkers/base.py index 55e5f36..497aa40 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -13,13 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """basic checker for Python code""" import sys import astroid from logilab.common.ureports import Table -from astroid import are_exclusive +from astroid import are_exclusive, InferenceError import astroid.bases from pylint.interfaces import IAstroidChecker @@ -52,11 +52,15 @@ NO_REQUIRED_DOC_RGX = re.compile('__.*__') REVERSED_METHODS = (('__getitem__', '__len__'), ('__reversed__', )) +PY33 = sys.version_info >= (3, 3) BAD_FUNCTIONS = ['map', 'filter', 'apply'] if sys.version_info < (3, 0): BAD_FUNCTIONS.append('input') BAD_FUNCTIONS.append('file') +# Name categories that are always consistent with all naming conventions. +EXEMPT_NAME_CATEGORIES = {'exempt', 'ignore'} + del re def in_loop(node): @@ -101,6 +105,8 @@ if sys.version_info < (3, 0): PROPERTY_CLASSES = set(('__builtin__.property', 'abc.abstractproperty')) else: PROPERTY_CLASSES = set(('builtins.property', 'abc.abstractproperty')) +ABC_METHODS = set(('abc.abstractproperty', 'abc.abstractmethod', + 'abc.abstractclassmethod', 'abc.abstractstaticmethod')) def _determine_function_name_type(node): """Determine the name type whose regex the a function's name should match. @@ -130,6 +136,25 @@ def _determine_function_name_type(node): return 'attr' return 'method' +def decorated_with_abc(func): + """ Determine if the `func` node is decorated + with `abc` decorators (abstractmethod et co.) + """ + if func.decorators: + for node in func.decorators.nodes: + try: + infered = node.infer().next() + except InferenceError: + continue + if infered and infered.qname() in ABC_METHODS: + return True + +def has_abstract_methods(node): + """ Determine if the given `node` has + abstract methods, defined with `abc` module. + """ + return any(decorated_with_abc(meth) + for meth in node.mymethods()) def report_by_type_stats(sect, stats, old_stats): """make a report of @@ -220,7 +245,8 @@ class BasicErrorChecker(_BasicChecker): 'return-arg-in-generator', 'Used when a "return" statement with an argument is found ' 'outside in a generator function or method (e.g. with some ' - '"yield" statements).'), + '"yield" statements).', + {'maxversion': (3, 3)}), 'E0107': ("Use of the non-existent %s operator", 'nonexistent-operator', "Used when you attempt to use the C-style pre-increment or" @@ -229,6 +255,11 @@ class BasicErrorChecker(_BasicChecker): 'duplicate-argument-name', 'Duplicate argument names in function definitions are syntax' ' errors.'), + 'E0110': ('Abstract class with abstract methods instantiated', + 'abstract-class-instantiated', + 'Used when an abstract class with `abc.ABCMeta` as metaclass ' + 'has abstract methods and is instantiated.', + {'minversion': (3, 0)}), '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 ' @@ -266,11 +297,12 @@ class BasicErrorChecker(_BasicChecker): 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, astroid.Const) and \ - retnode.value.value is not None: - self.add_message('return-arg-in-generator', node=node, - line=retnode.fromlineno) + if not PY33: + for retnode in returns: + if isinstance(retnode.value, astroid.Const) and \ + retnode.value.value is not None: + self.add_message('return-arg-in-generator', node=node, + line=retnode.fromlineno) # Check for duplicate names args = set() for name in node.argnames(): @@ -314,6 +346,34 @@ class BasicErrorChecker(_BasicChecker): (node.operand.op == node.op)): self.add_message('nonexistent-operator', node=node, args=node.op*2) + @check_messages('abstract-class-instantiated') + def visit_callfunc(self, node): + """ Check instantiating abstract class with + abc.ABCMeta as metaclass. + """ + try: + infered = node.func.infer().next() + except astroid.InferenceError: + return + if not isinstance(infered, astroid.Class): + return + # __init__ was called + metaclass = infered.metaclass() + if metaclass is None: + # Python 3.4 has `abc.ABC`, which won't be detected + # by ClassNode.metaclass() + for ancestor in infered.ancestors(): + if (ancestor.qname() == 'abc.ABC' and + has_abstract_methods(infered)): + + self.add_message('abstract-class-instantiated', node=node) + break + return + if (metaclass.qname() == 'abc.ABCMeta' and + has_abstract_methods(infered)): + + self.add_message('abstract-class-instantiated', node=node) + 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): @@ -393,6 +453,12 @@ functions, methods '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 !'), + 'W0123': ('Use of eval', + 'eval-used', + 'Used when you use the "eval" function, to discourage its ' + 'usage. Consider using `ast.literal_eval` for safely evaluating ' + 'strings containing Python expressions ' + 'from untrusted sources. '), 'W0141': ('Used builtin function %r', 'bad-builtin', 'Used when a black listed builtin function is used (see the ' @@ -623,7 +689,7 @@ functions, methods """just print a warning on exec statements""" self.add_message('exec-used', node=node) - @check_messages('bad-builtin', 'star-args', + @check_messages('bad-builtin', 'star-args', 'eval-used', 'exec-used', 'missing-reversed-argument', 'bad-reversed-sequence') def visit_callfunc(self, node): @@ -640,6 +706,8 @@ functions, methods self.add_message('exec-used', node=node) elif name == 'reversed': self._check_reversed(node) + elif name == 'eval': + self.add_message('eval-used', node=node) if name in self.config.bad_functions: self.add_message('bad-builtin', node=node, args=name) if node.starargs or node.kwargs: @@ -754,79 +822,47 @@ functions, methods # everything else is not a proper sequence for reversed() self.add_message('bad-reversed-sequence', node=node) +_NAME_TYPES = { + 'module': (MOD_NAME_RGX, 'module'), + 'const': (CONST_NAME_RGX, 'constant'), + 'class': (CLASS_NAME_RGX, 'class'), + 'function': (DEFAULT_NAME_RGX, 'function'), + 'method': (DEFAULT_NAME_RGX, 'method'), + 'attr': (DEFAULT_NAME_RGX, 'attribute'), + 'argument': (DEFAULT_NAME_RGX, 'argument'), + 'variable': (DEFAULT_NAME_RGX, 'variable'), + 'class_attribute': (CLASS_ATTRIBUTE_RGX, 'class attribute'), + 'inlinevar': (COMP_VAR_RGX, 'inline iteration'), +} + +def _create_naming_options(): + name_options = [] + for name_type, (rgx, human_readable_name) in _NAME_TYPES.iteritems(): + name_type = name_type.replace('_', '-') + name_options.append(( + '%s-rgx' % (name_type,), + {'default': rgx, 'type': 'regexp', 'metavar': '<regexp>', + 'help': 'Regular expression matching correct %s names' % (human_readable_name,)})) + name_options.append(( + '%s-name-hint' % (name_type,), + {'default': rgx.pattern, 'type': 'string', 'metavar': '<string>', + 'help': 'Naming hint for %s names' % (human_readable_name,)})) + + return tuple(name_options) + 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 %s name "%s"', + 'C0103': ('Invalid %s name "%s"%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' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'module names'} - ), - ('const-rgx', - {'default' : CONST_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'module level names'} - ), - ('class-rgx', - {'default' : CLASS_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'class names'} - ), - ('function-rgx', - {'default' : DEFAULT_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'function names'} - ), - ('method-rgx', - {'default' : DEFAULT_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'method names'} - ), - ('attr-rgx', - {'default' : DEFAULT_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'instance attribute names'} - ), - ('argument-rgx', - {'default' : DEFAULT_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'argument names'}), - ('variable-rgx', - {'default' : DEFAULT_NAME_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'variable names'} - ), - ('class-attribute-rgx', - {'default' : CLASS_ATTRIBUTE_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'attribute names in class bodies'} - ), - ('inlinevar-rgx', - {'default' : COMP_VAR_RGX, - 'type' :'regexp', 'metavar' : '<regexp>', - 'help' : 'Regular expression which should only match correct ' - 'list comprehension / generator expression variable \ - names'} - ), - # XXX use set + options = (# XXX use set ('good-names', {'default' : ('i', 'j', 'k', 'ex', 'Run', '_'), 'type' :'csv', 'metavar' : '<names>', @@ -839,7 +875,24 @@ class NameChecker(_BasicChecker): 'help' : 'Bad variable names which should always be refused, ' 'separated by a comma'} ), - ) + ('name-group', + {'default' : (), + 'type' :'csv', 'metavar' : '<name1:name2>', + 'help' : ('Colon-delimited sets of names that determine each' + ' other\'s naming style when the name regexes' + ' allow several styles.')} + ), + ('include-naming-hint', + {'default': False, 'type' : 'yn', 'metavar' : '<y_or_n>', + 'help': 'Include a hint for the correct naming format with invalid-name'} + ), + ) + _create_naming_options() + + + def __init__(self, linter): + _BasicChecker.__init__(self, linter) + self._name_category = {} + self._name_group = {} def open(self): self.stats = self.linter.add_stats(badname_module=0, @@ -850,6 +903,9 @@ class NameChecker(_BasicChecker): badname_inlinevar=0, badname_argument=0, badname_class_attribute=0) + for group in self.config.name_group: + for name_type in group.split(':'): + self._name_group[name_type] = 'group_%s' % (group,) @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): @@ -911,6 +967,14 @@ class NameChecker(_BasicChecker): else: self._recursive_check_names(arg.elts, node) + def _find_name_group(self, node_type): + return self._name_group.get(node_type, node_type) + + def _is_multi_naming_match(self, match): + return (match is not None and + match.lastgroup is not None and + match.lastgroup not in EXEMPT_NAME_CATEGORIES) + def _check_name(self, node_type, name, node): """check for a name using the type's regexp""" if is_inside_except(node): @@ -924,13 +988,21 @@ class NameChecker(_BasicChecker): 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('invalid-name', node=node, args=(type_label, name)) + match = regexp.match(name) + + if self._is_multi_naming_match(match): + name_group = self._find_name_group(node_type) + if name_group not in self._name_category: + self._name_category[name_group] = match.lastgroup + elif self._name_category[name_group] != match.lastgroup: + match = None + + if match is None: + type_label = _NAME_TYPES[node_type][1] + hint = '' + if self.config.include_naming_hint: + hint = ' (hint: %s)' % (getattr(self.config, node_type + '_name_hint')) + self.add_message('invalid-name', node=node, args=(type_label, name, hint)) self.stats['badname_' + node_type] += 1 @@ -987,15 +1059,17 @@ class DocStringChecker(_BasicChecker): isinstance(ancestor[node.name], astroid.Function): overridden = True break - if not overridden: - self._check_docstring(ftype, node) + self._check_docstring(ftype, node, + report_missing=not overridden) else: self._check_docstring(ftype, node) - def _check_docstring(self, node_type, node): + def _check_docstring(self, node_type, node, report_missing=True): """check the node has a non empty docstring""" docstring = node.doc if docstring is None: + if not report_missing: + return if node.body: lines = node.body[-1].lineno - node.body[0].lineno + 1 else: diff --git a/checkers/classes.py b/checkers/classes.py index fc09021..85a8507 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """classes checker for Python code """ from __future__ import generators @@ -30,6 +30,7 @@ if sys.version_info >= (3, 0): NEXT_METHOD = '__next__' else: NEXT_METHOD = 'next' +ITER_METHODS = ('__iter__', '__getitem__') def class_is_abstract(node): """return true if the given class node should be considered as an abstract @@ -49,7 +50,7 @@ MSGS = { compatibility for an unexpected reason. Please report this kind \ if you don\'t make sense of it.'), - 'E0202': ('An attribute affected in %s line %s hide this method', + 'E0202': ('An attribute defined in %s line %s hides this method', 'method-hidden', 'Used when a class defines a method which is hidden by an ' 'instance attribute from an ancestor class or set by some ' @@ -156,7 +157,15 @@ MSGS = { 'bad-context-manager', 'Used when the __exit__ special method, belonging to a \ context manager, does not accept 3 arguments \ - (type, value, traceback).') + (type, value, traceback).'), + 'E0236': ('Invalid object %r in __slots__, must contain ' + 'only non empty strings', + 'invalid-slots-object', + 'Used when an invalid (non-string) object occurs in __slots__.'), + 'E0238': ('Invalid __slots__ object', + 'invalid-slots', + 'Used when an invalid __slots__ is found in class. ' + 'Only a string, an iterable or a sequence is permitted.') } @@ -240,6 +249,7 @@ a metaclass class method.'} node.local_attr('__init__') except astroid.NotFoundError: self.add_message('W0232', args=node, node=node) + self._check_slots(node) @check_messages('E0203', 'W0201') def leave_class(self, cnode): @@ -333,6 +343,49 @@ a metaclass class method.'} elif node.name == '__exit__': self._check_exit(node) + def _check_slots(self, node): + if '__slots__' not in node.locals: + return + for slots in node.igetattr('__slots__'): + # check if __slots__ is a valid type + for meth in ITER_METHODS: + try: + slots.getattr(meth) + break + except astroid.NotFoundError: + continue + else: + self.add_message('invalid-slots', node=node) + continue + + if isinstance(slots, astroid.Const): + # a string, ignore the following checks + continue + if not hasattr(slots, 'itered'): + # we can't obtain the values, maybe a .deque? + continue + + if isinstance(slots, astroid.Dict): + values = [item[0] for item in slots.items] + else: + values = slots.itered() + if values is YES: + return + + for elt in values: + if elt is YES: + continue + if (not isinstance(elt, astroid.Const) or + not isinstance(elt.value, str)): + self.add_message('invalid-slots-object', + args=elt.as_string(), + node=elt) + continue + if not elt.value: + self.add_message('invalid-slots-object', + args=elt.as_string(), + node=elt) + def _check_iter(self, node): try: infered = node.infer_call_result(node) @@ -568,6 +621,9 @@ a metaclass class method.'} continue # owner is not this class, it must be a parent class # check that the ancestor's method is not abstract + if method.name in node.locals: + # it is redefined as an attribute or with a descriptor + continue if method.is_abstract(pass_is_abstract=False): self.add_message('W0223', node=node, args=(method.name, owner.name)) diff --git a/checkers/design_analysis.py b/checkers/design_analysis.py index 11defbf..cfd2d80 100644 --- a/checkers/design_analysis.py +++ b/checkers/design_analysis.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """check for signs of poor design""" from astroid import Function, If, InferenceError diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 367aee2..30b3fa8 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """exceptions handling (raising, catching, exceptions classes) checker """ import sys @@ -134,8 +134,8 @@ class ExceptionsChecker(BaseChecker): ), ) - @check_messages('W0701', 'W0710', 'E0702', 'E0710', 'E0711', - 'bad-exception-context') + @check_messages('raising-string', 'nonstandard-exception', 'raising-bad-type', + 'raising-non-exception', 'notimplemented-raised', 'bad-exception-context') def visit_raise(self, node): """visit raise possibly inferring value""" # ignore empty raise @@ -172,22 +172,22 @@ class ExceptionsChecker(BaseChecker): if isinstance(expr, astroid.Const): value = expr.value if isinstance(value, str): - self.add_message('W0701', node=node) + self.add_message('raising-string', node=node) else: - self.add_message('E0702', node=node, + self.add_message('raising-bad-type', node=node, args=value.__class__.__name__) elif (isinstance(expr, astroid.Name) and \ expr.name in ('None', 'True', 'False')) or \ isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple, astroid.Module, astroid.Function)): - self.add_message('E0702', node=node, args=expr.name) + self.add_message('raising-bad-type', node=node, args=expr.name) 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) + self.add_message('notimplemented-raised', node=node) elif isinstance(expr, astroid.BinOp) and expr.op == '%': - self.add_message('W0701', node=node) + self.add_message('raising-string', node=node) elif isinstance(expr, (Instance, astroid.Class)): if isinstance(expr, Instance): expr = expr._proxied @@ -195,23 +195,24 @@ class ExceptionsChecker(BaseChecker): not inherit_from_std_ex(expr) and expr.root().name != BUILTINS_NAME): if expr.newstyle: - self.add_message('E0710', node=node) + self.add_message('raising-non-exception', node=node) else: - self.add_message('W0710', node=node) + self.add_message('nonstandard-exception', node=node) else: value_found = False else: value_found = False return value_found - @check_messages('W0712') + @check_messages('unpacking-in-except') 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) + self.add_message('unpacking-in-except', node=node) - @check_messages('W0702', 'W0703', 'W0704', 'W0711', 'E0701', 'catching-non-exception') + @check_messages('bare-except', 'broad-except', 'pointless-except', 'binary-op-exception', 'bad-except-order', + 'catching-non-exception') def visit_tryexcept(self, node): """check for empty except""" exceptions_classes = [] @@ -219,18 +220,18 @@ class ExceptionsChecker(BaseChecker): for index, handler in enumerate(node.handlers): # single except doing nothing but "pass" without else clause if nb_handlers == 1 and is_empty(handler.body) and not node.orelse: - self.add_message('W0704', node=handler.type or handler.body[0]) + self.add_message('pointless-except', node=handler.type or handler.body[0]) if handler.type is None: if nb_handlers == 1 and not is_raising(handler.body): - self.add_message('W0702', node=handler) + self.add_message('bare-except', node=handler) # check if a "except:" is followed by some other # except elif index < (nb_handlers - 1): msg = 'empty except clause should always appear last' - self.add_message('E0701', node=node, args=msg) + self.add_message('bad-except-order', node=node, args=msg) elif isinstance(handler.type, astroid.BoolOp): - self.add_message('W0711', node=handler, args=handler.type.op) + self.add_message('binary-op-exception', node=handler, args=handler.type.op) else: try: excs = list(unpack_infer(handler.type)) @@ -246,11 +247,11 @@ class ExceptionsChecker(BaseChecker): if previous_exc in exc_ancestors: msg = '%s is an ancestor class of %s' % ( previous_exc.name, exc.name) - self.add_message('E0701', node=handler.type, args=msg) + self.add_message('bad-except-order', node=handler.type, args=msg) if (exc.name in self.config.overgeneral_exceptions and exc.root().name == EXCEPTIONS_MODULE and nb_handlers == 1 and not is_raising(handler.body)): - self.add_message('W0703', args=exc.name, node=handler.type) + self.add_message('broad-except', args=exc.name, node=handler.type) if (not inherit_from_std_ex(exc) and exc.root().name != BUILTINS_NAME): diff --git a/checkers/format.py b/checkers/format.py index aab2320..abe38a5 100644 --- a/checkers/format.py +++ b/checkers/format.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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Python code format's checker. By default try to follow Guido's style guide : @@ -344,7 +344,7 @@ class FormatChecker(BaseTokenChecker): return ':' elif tokens[i][1] in '()[]{}': return 'bracket' - elif tokens[i][1] in ('<', '>', '<=', '>=', '!='): + elif tokens[i][1] in ('<', '>', '<=', '>=', '!=', '=='): return 'comparison' else: if self._inside_brackets('('): diff --git a/checkers/imports.py b/checkers/imports.py index b0a9872..fd897a8 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -12,11 +12,11 @@ # # 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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """imports checkers for Python code""" from logilab.common.graph import get_cycles, DotBackend -from logilab.common.modutils import is_standard_module +from logilab.common.modutils import get_module_part, is_standard_module from logilab.common.ureports import VerbatimText, Paragraph import astroid @@ -248,6 +248,9 @@ given file (report RP0402 must not be disabled)'} and prev.modname == '__future__'): self.add_message('W0410', node=node) return + for name, _ in node.names: + if name == '*': + self.add_message('W0401', args=basename, node=node) modnode = node.root() importedmodnode = self.get_imported_module(modnode, node, basename) if importedmodnode is None: @@ -255,11 +258,9 @@ given file (report RP0402 must not be disabled)'} self._check_relative_import(modnode, node, importedmodnode, basename) self._check_deprecated_module(node, basename) for name, _ in node.names: - if name == '*': - self.add_message('W0401', args=basename, node=node) - continue - self._add_imported_module(node, '%s.%s' % (importedmodnode.name, name)) - self._check_reimport(node, name, basename, node.level) + if name != '*': + self._add_imported_module(node, '%s.%s' % (importedmodnode.name, name)) + self._check_reimport(node, name, basename, node.level) def get_imported_module(self, modnode, importnode, modname): try: @@ -291,6 +292,7 @@ given file (report RP0402 must not be disabled)'} def _add_imported_module(self, node, importedmodname): """notify an imported module, used to analyze dependencies""" + importedmodname = get_module_part(importedmodname) context_name = node.root().name if context_name == importedmodname: # module importing itself ! diff --git a/checkers/logging.py b/checkers/logging.py index d1f9d36..cc8967d 100644 --- a/checkers/logging.py +++ b/checkers/logging.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """checker for use of Python logging """ @@ -61,21 +61,45 @@ class LoggingChecker(checkers.BaseChecker): name = 'logging' msgs = MSGS + options = (('logging-modules', + {'default' : ('logging',), + 'type' : 'csv', + 'metavar' : '<comma separated list>', + 'help' : ('Logging modules to check that the string format ' + 'arguments are in logging function parameter format')} + ), + ) + def visit_module(self, unused_node): """Clears any state left in this checker from last module checked.""" # The code being checked can just as easily "import logging as foo", # so it is necessary to process the imports and store in this field # what name the logging module is actually given. - self._logging_name = None + self._logging_names = set() + logging_mods = self.config.logging_modules + + self._logging_modules = set(logging_mods) + self._from_imports = {} + for logging_mod in logging_mods: + parts = logging_mod.rsplit('.', 1) + if len(parts) > 1: + self._from_imports[parts[0]] = parts[1] + + def visit_from(self, node): + """Checks to see if a module uses a non-Python logging module.""" + try: + logging_name = self._from_imports[node.modname] + for module, as_name in node.names: + if module == logging_name: + self._logging_names.add(as_name or module) + except KeyError: + pass def visit_import(self, node): """Checks to see if this module uses Python's built-in logging.""" for module, as_name in node.names: - if module == 'logging': - if as_name: - self._logging_name = as_name - else: - self._logging_name = 'logging' + if module in self._logging_modules: + self._logging_names.add(as_name or module) @check_messages(*(MSGS.keys())) def visit_callfunc(self, node): @@ -91,7 +115,7 @@ class LoggingChecker(checkers.BaseChecker): and ancestor.parent.name == 'logging')))] except astroid.exceptions.InferenceError: return - if node.func.expr.name != self._logging_name and not logger_class: + if node.func.expr.name not in self._logging_names and not logger_class: return self._check_convenience_methods(node) self._check_log_methods(node) diff --git a/checkers/misc.py b/checkers/misc.py index 6995909..9c49825 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Copyright (c) 2000-2010 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr diff --git a/checkers/newstyle.py b/checkers/newstyle.py index ff9bbc2..b6cb664 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """check for new / old style related problems """ import sys @@ -127,8 +127,8 @@ class NewStyleConflictChecker(BaseChecker): continue if klass is not supcls: - supcls = getattr(supcls, 'name', supcls) - self.add_message('E1003', node=call, args=(supcls, )) + self.add_message('E1003', node=call, + args=(call.args[0].name, )) def register(linter): diff --git a/checkers/raw_metrics.py b/checkers/raw_metrics.py index 23e45b0..71fecf6 100644 --- a/checkers/raw_metrics.py +++ b/checkers/raw_metrics.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr diff --git a/checkers/similar.py b/checkers/similar.py index 8d755fa..cf671bf 100644 --- a/checkers/similar.py +++ b/checkers/similar.py @@ -13,7 +13,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """a similarities / code duplication command line tool and pylint checker """ import sys diff --git a/checkers/stdlib.py b/checkers/stdlib.py index b63760c..8cb78f4 100644 --- a/checkers/stdlib.py +++ b/checkers/stdlib.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Checkers for various standard library functions.""" import re diff --git a/checkers/strings.py b/checkers/strings.py index c6bf960..663d61d 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -14,7 +14,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Checker for string formatting operations. """ @@ -102,15 +102,15 @@ class StringFormatChecker(BaseChecker): utils.parse_format_string(format_string) except utils.UnsupportedFormatCharacter, e: c = format_string[e.index] - self.add_message('E1300', node=node, args=(c, ord(c), e.index)) + self.add_message('bad-format-character', node=node, args=(c, ord(c), e.index)) return except utils.IncompleteFormatString: - self.add_message('E1301', node=node) + self.add_message('truncated-format-string', node=node) return if required_keys and required_num_args: # The format string uses both named and unnamed format # specifiers. - self.add_message('E1302', node=node) + self.add_message('mixed-format-string', node=node) elif required_keys: # The format string uses only named format specifiers. # Check that the RHS of the % operator is a mapping object @@ -125,7 +125,7 @@ class StringFormatChecker(BaseChecker): if isinstance(key, basestring): keys.add(key) else: - self.add_message('W1300', node=node, args=key) + self.add_message('bad-format-string-key', node=node, args=key) else: # One of the keys was something other than a # constant. Since we can't tell what it is, @@ -135,13 +135,13 @@ class StringFormatChecker(BaseChecker): if not unknown_keys: for key in required_keys: if key not in keys: - self.add_message('E1304', node=node, args=key) + self.add_message('missing-format-string-key', node=node, args=key) for key in keys: if key not in required_keys: - self.add_message('W1301', node=node, args=key) + self.add_message('unused-format-string-key', node=node, args=key) elif isinstance(args, OTHER_NODES + (astroid.Tuple,)): type_name = type(args).__name__ - self.add_message('E1303', node=node, args=type_name) + self.add_message('format-needs-mapping', node=node, args=type_name) # else: # The RHS of the format specifier is a name or # expression. It may be a mapping object, so @@ -162,9 +162,9 @@ class StringFormatChecker(BaseChecker): num_args = None if num_args is not None: if num_args > required_num_args: - self.add_message('E1305', node=node) + self.add_message('too-many-format-args', node=node) elif num_args < required_num_args: - self.add_message('E1306', node=node) + self.add_message('too-few-format-args', node=node) class StringMethodsChecker(BaseChecker): @@ -189,7 +189,7 @@ class StringMethodsChecker(BaseChecker): if not isinstance(arg, astroid.Const): return if len(arg.value) != len(set(arg.value)): - self.add_message('E1310', node=node, + self.add_message('bad-str-strip-call', node=node, args=(func.bound.name, func.name)) @@ -282,9 +282,11 @@ class StringConstantChecker(BaseTokenChecker): elif _PY3K and 'b' not in prefix: pass # unicode by default else: - self.add_message('W1402', line=start_row, args=(match, )) + self.add_message('anomalous-unicode-escape-in-string', + line=start_row, args=(match, )) elif next_char not in self.ESCAPE_CHARACTERS: - self.add_message('W1401', line=start_row, args=(match, )) + self.add_message('anomalous-backslash-in-string', + 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. diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 2e3785e..a775e6c 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """try to find more bugs in the code using astroid inference capabilities """ @@ -48,34 +48,81 @@ MSGS = { 'Used when an assignment is done on a function call but the \ inferred function returns nothing but None.'), - 'E1120': ('No value passed for parameter %s in function call', + 'E1120': ('No value for argument %s in %s call', 'no-value-for-parameter', 'Used when a function call passes too few arguments.'), - 'E1121': ('Too many positional arguments for function call', + 'E1121': ('Too many positional arguments for %s call', 'too-many-function-args', 'Used when a function call passes too many positional \ arguments.'), - 'E1122': ('Duplicate keyword argument %r in function call', + 'E1122': ('Duplicate keyword argument %r in %s call', 'duplicate-keyword-arg', 'Used when a function call passes the same keyword argument \ multiple times.', {'maxversion': (2, 6)}), - 'E1123': ('Passing unexpected keyword argument %r in function call', + 'E1123': ('Unexpected keyword argument %r in %s call', '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': ('Parameter %r passed as both positional and keyword argument', + 'E1124': ('Argument %r passed by position and keyword in %s call', 'redundant-keyword-arg', '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', + 'E1125': ('Missing mandatory keyword argument %r in %s call', 'missing-kwoa', - 'Used when a function call doesn\'t pass a mandatory \ - keyword-only argument.', + ('Used when a function call does not pass a mandatory' + ' keyword-only argument.'), {'minversion': (3, 0)}), } +def _determine_callable(callable_obj): + # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must + # come first in this 'if..else'. + if isinstance(callable_obj, astroid.BoundMethod): + # Bound methods have an extra implicit 'self' argument. + return callable_obj, 1, 'method' + elif isinstance(callable_obj, astroid.UnboundMethod): + if callable_obj.decorators is not None: + for d in callable_obj.decorators.nodes: + if isinstance(d, astroid.Name) and d.name == 'classmethod': + # Class methods have an extra implicit 'cls' argument. + return called, 1, 'class method' + elif isinstance(d, astroid.Name) and d.name == 'staticmethod': + return called, 0, 'static method' + else: + return callable_obj, 0, 'unbound method' + elif isinstance(callable_obj, astroid.Function): + return callable_obj, 0, 'function' + elif isinstance(callable_obj, astroid.Lambda): + return callable_obj, 0, 'lambda' + elif isinstance(callable_obj, astroid.Class): + # Class instantiation, lookup __new__ instead. + # If we only find object.__new__, we can safely check __init__ + # instead. + try: + # Use the last definition of __new__. + new = callable_obj.local_attr('__new__')[-1] + except astroid.NotFoundError: + new = None + + if not new or new.parent.name == 'object': + try: + # Use the last definition of __init__. + callable_obj = callable_obj.local_attr('__init__')[-1] + except astroid.NotFoundError: + # do nothing, covered by no-init. + raise ValueError + else: + callable_obj = new + + if not isinstance(callable_obj, astroid.Function): + raise ValueError + # both have an extra implicit 'cls'/'self' argument. + return callable_obj, 1, 'constructor' + else: + raise ValueError + class TypeChecker(BaseChecker): """try to find bugs in the code using type inference """ @@ -132,7 +179,7 @@ accessed. Python regular expressions are accepted.'} def visit_delattr(self, node): self.visit_getattr(node) - @check_messages('E1101', 'E1103') + @check_messages('no-member', 'maybe-no-member') def visit_getattr(self, node): """check that the accessed attribute exists @@ -211,14 +258,14 @@ accessed. Python regular expressions are accepted.'} continue done.add(actual) if inference_failure: - msgid = 'E1103' + msgid = 'maybe-no-member' else: - msgid = 'E1101' + msgid = 'no-member' self.add_message(msgid, node=node, args=(owner.display_type(), name, node.attrname)) - @check_messages('E1111', 'W1111') + @check_messages('assignment-from-no-return', 'assignment-from-none') def visit_assign(self, node): """check that if assigning to a function call, the function is possibly returning something valuable @@ -236,14 +283,14 @@ accessed. Python regular expressions are accepted.'} returns = list(function_node.nodes_of_class(astroid.Return, skip_klass=astroid.Function)) if len(returns) == 0: - self.add_message('E1111', node=node) + self.add_message('assignment-from-no-return', node=node) else: for rnode in returns: if not (isinstance(rnode.value, astroid.Const) and rnode.value.value is None): break else: - self.add_message('W1111', node=node) + self.add_message('assignment-from-none', node=node) @check_messages(*(MSGS.keys())) def visit_callfunc(self, node): @@ -251,7 +298,6 @@ accessed. Python regular expressions are accepted.'} and that the arguments passed to the function match the parameters in the inferred function's definition """ - # Build the set of keyword arguments, checking for duplicate keywords, # and count the positional arguments. keyword_args = set() @@ -260,7 +306,7 @@ accessed. Python regular expressions are accepted.'} if isinstance(arg, astroid.Keyword): keyword = arg.arg if keyword in keyword_args: - self.add_message('E1122', node=node, args=keyword) + self.add_message('duplicate-keyword-arg', node=node, args=keyword) keyword_args.add(keyword) else: num_positional_args += 1 @@ -268,26 +314,15 @@ accessed. Python regular expressions are accepted.'} called = safe_infer(node.func) # only function, generator and object defining __call__ are allowed if called is not None and not called.callable(): - self.add_message('E1102', node=node, args=node.func.as_string()) - - # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must - # come first in this 'if..else'. - if isinstance(called, astroid.BoundMethod): - # Bound methods have an extra implicit 'self' argument. - num_positional_args += 1 - elif isinstance(called, astroid.UnboundMethod): - if called.decorators is not None: - for d in called.decorators.nodes: - 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, astroid.Function) or - isinstance(called, astroid.Lambda)): - pass - else: - return + self.add_message('not-callable', node=node, args=node.func.as_string()) + try: + called, implicit_args, callable_name = _determine_callable(called) + except ValueError: + # Any error occurred during determining the function type, most of + # those errors are handled by different warnings. + return + num_positional_args += implicit_args if called.args.args is None: # Built-in functions have no argument information. return @@ -342,7 +377,7 @@ accessed. Python regular expressions are accepted.'} break else: # Too many positional arguments. - self.add_message('E1121', node=node) + self.add_message('too-many-function-args', node=node, args=(callable_name,)) break # 2. Match the keyword arguments. @@ -351,13 +386,13 @@ accessed. Python regular expressions are accepted.'} i = parameter_name_to_index[keyword] if parameters[i][1]: # Duplicate definition of function parameter. - self.add_message('E1124', node=node, args=keyword) + self.add_message('redundant-keyword-arg', node=node, args=(keyword, callable_name)) 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) + self.add_message('redundant-keyword-arg', node=node, args=(keyword, callable_name)) else: kwparams[keyword][1] = True elif called.args.kwarg is not None: @@ -365,7 +400,7 @@ accessed. Python regular expressions are accepted.'} pass else: # Unexpected keyword argument. - self.add_message('E1123', node=node, args=keyword) + self.add_message('unexpected-keyword-arg', node=node, args=(keyword, callable_name)) # 3. Match the *args, if any. Note that Python actually processes # *args _before_ any keyword arguments, but we wait until after @@ -402,12 +437,12 @@ accessed. Python regular expressions are accepted.'} display_name = '<tuple>' else: display_name = repr(name) - self.add_message('E1120', node=node, args=display_name) + self.add_message('no-value-for-parameter', node=node, args=(display_name, callable_name)) for name in kwparams: defval, assigned = kwparams[name] if defval is None and not assigned: - self.add_message('E1125', node=node, args=name) + self.add_message('missing-kwoa', node=node, args=(name, callable_name)) def register(linter): diff --git a/checkers/utils.py b/checkers/utils.py index 728893e..e7d85d4 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -14,7 +14,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """some functions that may be useful for various checkers """ @@ -407,7 +407,7 @@ def get_argument_from_call(callfunc_node, position=None, keyword=None): try: if position is not None and not isinstance(callfunc_node.args[position], astroid.Keyword): return callfunc_node.args[position] - except IndexError as error: + except IndexError, error: raise NoSuchArgumentError(error) if keyword: for arg in callfunc_node.args: diff --git a/checkers/variables.py b/checkers/variables.py index 90b7fe7..7c489e8 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -12,16 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """variables checkers for Python code """ - +import os import sys from copy import copy import astroid from astroid import are_exclusive, builtin_lookup, AstroidBuildingException +from logilab.common.modutils import file_from_modpath + from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker from pylint.checkers.utils import (PYMETHODS, is_ancestor_name, is_builtin, @@ -217,7 +219,25 @@ builtins. Remember that you should avoid to define new builtins when possible.' del not_consumed[elt_name] continue if elt_name not in node.locals: - self.add_message('E0603', args=elt_name, node=elt) + if not node.package: + self.add_message('undefined-all-variable', + args=elt_name, + node=elt) + else: + basename = os.path.splitext(node.file)[0] + if os.path.basename(basename) == '__init__': + name = node.name + "." + elt_name + try: + file_from_modpath(name.split(".")) + except ImportError: + self.add_message('undefined-all-variable', + args=elt_name, + node=elt) + except SyntaxError, exc: + # don't yield an syntax-error warning, + # because it will be later yielded + # when the file will be checked + pass # don't check unused imports in __init__ files if not self.config.init_import and node.package: return @@ -507,6 +527,12 @@ builtins. Remember that you should avoid to define new builtins when possible.' # defined in global or builtin scope if defframe.root().lookup(name)[1]: maybee0601 = False + else: + # check if we have a nonlocal + if name in defframe.locals: + maybee0601 = not any(isinstance(child, astroid.Nonlocal) + and name in child.names + for child in defframe.get_children()) if (maybee0601 and stmt.fromlineno <= defstmt.fromlineno and not is_defined_before(node) @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """utilities for Pylint configuration : * pylintrc diff --git a/debian/control b/debian/control index ecc265b..313445a 100644 --- a/debian/control +++ b/debian/control @@ -19,7 +19,7 @@ Architecture: all Depends: ${python:Depends}, ${misc:Depends}, python-logilab-common (>= 0.53.0), - python-astroid + python-astroid (>= 1.0.1) Suggests: python-tk XB-Python-Version: ${python:Versions} Description: python code static checker and UML diagram generator diff --git a/doc/index.rst b/doc/index.rst index 790ff93..7b8725c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -13,8 +13,10 @@ https://bitbucket.org/logilab/pylint output message-control features + options extend ide-integration + plugins contribute tutorial @@ -29,12 +31,6 @@ Content wanted It would be nice to include in the documentation the following information: -- pylint plugins (how to write one, how to install a 3rd party plugin, how to - configure Pylint to run it) - - pylint brain project : what it is, how to install it... Please send your pull requests via bitbucket if you can help with the above. - - - diff --git a/doc/options.rst b/doc/options.rst new file mode 100644 index 0000000..4a159f2 --- /dev/null +++ b/doc/options.rst @@ -0,0 +1,139 @@ +.. -*- coding: utf-8 -*- + +=============== + Configuration +=============== + +Naming Styles +------------- + +PyLint recognizes a number of different name types internally. With a few +exceptions, the type of the name is governed by the location the assignment to a +name is found in, and not the type of object assigned. + +``module`` + Module and package names, same as the file names. +``const`` + Module-level constants, any variable defined at module level that is not bound to a class object. +``class`` + Names in ``class`` statements, as well as names bound to class objects at module level. +``function`` + Functions, toplevel or nested in functions or methods. +``method`` + Methods, functions defined in class bodies. Includes static and class methods. +``attr`` + Attributes created on class instances inside methods. +``argument`` + Arguments to any function type, including lambdas. +``variable`` + Local variables in function scopes. +``class-attribute`` + Attributes defined in class bodies. +``inlinevar`` + Loop variables in list comprehensions and generator expressions. + +For each naming style, a separate regular expression matching valid names of +this type can be defined. By default, the regular expressions will enforce PEP8 +names. + +Regular expressions for the names are anchored at the beginning, any anchor for +the end must be supplied explicitly. Any name not matching the regular +expression will lead to an instance of ``invalid-name``. + + +.. option:: --module-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --const-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --class-rgx=<regex> + + Default value: ``'[A-Z_][a-zA-Z0-9]+$`` + +.. option:: --function-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --method-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --attr-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --argument-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --variable-rgx=<regex> + + Default value: ``[a-z_][a-z0-9_]{2,30}$`` + +.. option:: --class-attribute-rgx=<regex> + + Default value: ``([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$`` + +.. option:: --inlinevar-rgx=<regex> + + Default value: ``[A-Za-z_][A-Za-z0-9_]*$`` + +Multiple Naming Styles +^^^^^^^^^^^^^^^^^^^^^^ + +Large code bases that have been worked on for multiple years often exhibit an +evolution in style as well. In some cases, modules can be in the same package, +but still have different naming style based on the stratum they belong to. +However, intra-module consistency should still be required, to make changes +inside a single file easier. For this case, PyLint supports regular expression +with several named capturing group. + +The capturing group of the first valid match taints the module and enforces the +same group to be triggered on every subsequent occurrence of this name. + +Consider the following (simplified) example:: + + pylint --function-rgx='(?:(?P<snake>[a-z_]+)|(?P<camel>_?[A-Z]+))$' sample.py + +The regular expression defines two naming styles, ``snake`` for snake-case +names, and ``camel`` for camel-case names. + +In ``sample.py``, the function name on line 1 will taint the module and enforce +the match of named group ``snake`` for the remainder of the module:: + + def trigger_snake_case(arg): + ... + + def InvalidCamelCase(arg): + ... + + def valid_snake_case(arg): + ... + +Because of this, the name on line 4 will trigger an ``invalid-name`` warning, +even though the name matches the given regex. + +Matches named ``exempt`` or ``ignore`` can be used for non-tainting names, to +prevent built-in or interface-dictated names to trigger certain naming styles. + +.. option:: --name-group=<name1:name2:...,...> + + Default value: empty + + Format: comma-separated groups of colon-separated names. + + This option can be used to combine name styles. For example, ``function:method`` enforces that functions and methods use the same style, and a style triggered by either name type carries over to the other. This requires that the regular expression for the combined name types use the same group names. + +Name Hints +^^^^^^^^^^ + +.. option:: --include-naming-hint=y|n + + Default: off + + Include a hint for the correct name format with every ``invalid-name`` warning. + + Name hints default to the regular expression, but can be separately configured with the ``--<name-type>-hint`` options. diff --git a/doc/plugins.rst b/doc/plugins.rst new file mode 100644 index 0000000..052c13f --- /dev/null +++ b/doc/plugins.rst @@ -0,0 +1,122 @@ +.. -*- coding: utf-8 -*-
+
+=======
+Plugins
+=======
+
+Why write a plugin?
+-------------------
+
+Pylint is a static analysis tool and Python is a dynamically typed language.
+So there will be cases where Pylint cannot analyze files properly (this problem
+can happen in statically typed languages also if reflection or dynamic
+evaluation is used). Plugin is a way to tell Pylint how to handle such cases,
+since only the user would know what needs to be done.
+
+Example
+-------
+
+Let us run Pylint on a module from the Python source: `warnings.py`_ and see what happens:
+
+.. sourcecode:: bash
+
+ amitdev$ pylint -E Lib/warnings.py
+ E:297,36: Instance of 'WarningMessage' has no 'message' member (no-member)
+ E:298,36: Instance of 'WarningMessage' has no 'filename' member (no-member)
+ E:298,51: Instance of 'WarningMessage' has no 'lineno' member (no-member)
+ E:298,64: Instance of 'WarningMessage' has no 'line' member (no-member)
+
+
+Did we catch a genuine error? Let's open the code and look at ``WarningMessage`` class:
+
+.. sourcecode:: python
+
+ class WarningMessage(object):
+
+ """Holds the result of a single showwarning() call."""
+
+ _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
+ "line")
+
+ def __init__(self, message, category, filename, lineno, file=None,
+ line=None):
+ local_values = locals()
+ for attr in self._WARNING_DETAILS:
+ setattr(self, attr, local_values[attr])
+ self._category_name = category.__name__ if category else None
+
+ def __str__(self):
+ ...
+
+Ah, the fields (``message``, ``category`` etc) are not defined statically on the class.
+Instead they are added using ``setattr``. Pylint would have a tough time figuring
+this out.
+
+Enter Plugin
+------------
+
+We can write a plugin to tell Pylint about how to analyze this properly. A
+plugin is a module which should have a function ``register`` and takes the
+`lint`_ module as input. So a basic hello-world plugin can be implemented as:
+
+.. sourcecode:: python
+
+ # Inside hello_plugin.py
+ def register(linter):
+ print 'Hello world'
+
+We can run this plugin by placing this module in the PYTHONPATH and invoking as:
+
+.. sourcecode:: bash
+
+ amitdev$ pylint -E --load-plugins hello_plugin foo.py
+ Hello world
+
+Back to our example: one way to fix that would be to transform the ``WarningMessage`` class
+and set the attributes using a plugin so that Pylint can see them. This can be done by
+registering a transform function. We can transform any node in the parsed AST like
+Module, Class, Function etc. In our case we need to transform a class. It can be done so:
+
+.. sourcecode:: python
+
+ from astroid import MANAGER
+ from astroid import scoped_nodes
+
+ def register(linter):
+ pass
+
+ def transform(cls):
+ if cls.name == 'WarningMessage':
+ import warnings
+ for f in warnings.WarningMessage._WARNING_DETAILS:
+ cls.locals[f] = [scoped_nodes.Class(f, None)]
+
+ MANAGER.register_transform(scoped_nodes.Class, transform)
+
+Let's go through the plugin. First, we need to register a class transform, which
+is done via the ``register_transform`` function in ``MANAGER``. It takes the node
+type and function as parameters. We need to change a class, so we use ``scoped_nodes.Class``.
+We also pass a ``transform`` function which does the actual transformation.
+
+``transform`` function is simple as well. If the class is ``WarningMessage`` then we
+add the attributes to its locals (we are not bothered about type of attributes, so setting
+them as class will do. But we could set them to any type we want). That's it.
+
+Note: We don't need to do anything in the ``register`` function of the plugin since we
+are not modifying anything in the linter itself.
+
+Lets run Pylint with this plugin and see:
+
+.. sourcecode:: bash
+
+ amitdev$ pylint -E --load-plugins warning_plugin Lib/warnings.py
+ amitdev$
+
+All the false positives associated with ``WarningMessage`` are now gone. This is just
+an example, any code transformation can be done by plugins. See `nodes`_ and `scoped_nodes`_
+for details about all node types that can be transformed.
+
+.. _`warnings.py`: http://hg.python.org/cpython/file/2.7/Lib/warnings.py
+.. _`scoped_nodes`: https://bitbucket.org/logilab/astroid/src/64026ffc0d94fe09e4bdc2bf5efaab29444645e7/scoped_nodes.py?at=default
+.. _`nodes`: https://bitbucket.org/logilab/astroid/src/64026ffc0d94fe09e4bdc2bf5efaab29444645e7/nodes.py?at=default
+.. _`lint`: https://bitbucket.org/logilab/pylint/src/f2acea7b640def0237513f66e3de5fa3de73f2de/lint.py?at=default
\ No newline at end of file diff --git a/elisp/pylint.el b/elisp/pylint.el index 09c22e6..17132e4 100644 --- a/elisp/pylint.el +++ b/elisp/pylint.el @@ -19,8 +19,8 @@ ;; ;; You should have received a copy of the GNU General Public License along ;; with your copy of Emacs; see the file COPYING. If not, write to the Free -;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA -;; 02111-1307, USA. +;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +;; MA 02110-1301, USA ;;; Commentary: ;; @@ -13,7 +13,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Emacs and Flymake compatible Pylint. This script is for integration with emacs and is compatible with flymake mode. @@ -81,29 +81,16 @@ def lint(filename, options=None): from pylint import lint as lint_mod lint_path = lint_mod.__file__ options = options or ['--disable=C,R,I'] - cmd = [sys.executable, lint_path] + options + ['--msg-template', - '{path}:{line}: [{symbol}, {obj}] {msg}', '-r', 'n', child_path] + cmd = [sys.executable, lint_path] + options + [ + '--msg-template', '{path}:{line}: {category} ({msg_id}, {symbol}, {obj}) {msg}', + '-r', 'n', child_path] process = Popen(cmd, stdout=PIPE, cwd=parent_path, universal_newlines=True) - # The parseable line format is '%(path)s:%(line)s: [%(sigle)s%(obj)s] %(msg)s' - # NOTE: This would be cleaner if we added an Emacs reporter to pylint.reporters.text .. - regex = re.compile(r"\[(?P<type>[WE])(?P<remainder>.*?)\]") - - def _replacement(match_object): - "Alter to include 'Error' or 'Warning'" - if match_object.group("type") == "W": - replacement = "Warning" - else: - replacement = "Error" - # replace as "Warning (W0511, funcName): Warning Text" - return "%s (%s%s):" % (replacement, match_object.group("type"), - match_object.group("remainder")) - for line in process.stdout: # remove pylintrc warning if line.startswith("No config file found"): continue - line = regex.sub(_replacement, line, 1) + # modify the file name thats output to reverse the path traversal we made parts = line.split(":") if parts and parts[0] == child_path: @@ -174,7 +161,7 @@ def Run(): print "%s does not exist" % sys.argv[1] sys.exit(1) else: - sys.exit(lint(sys.argv[1], sys.argv[1:])) + sys.exit(lint(sys.argv[1], sys.argv[2:])) if __name__ == '__main__': @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Tkinker gui for pylint""" import os diff --git a/interfaces.py b/interfaces.py index e0754ce..50f2c83 100644 --- a/interfaces.py +++ b/interfaces.py @@ -9,7 +9,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Interfaces for PyLint objects""" from logilab.common.interface import Interface @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2014 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 @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ %prog [options] module_or_package Check that a module satisfies a coding standard (and more !). @@ -29,6 +29,7 @@ # import this first to avoid builtin namespace pollution from pylint.checkers import utils +import functools import sys import os import tokenize @@ -389,7 +390,7 @@ warning, statement which respectively contain the number of errors / warnings\ value = check_csv(None, optname, value) if isinstance(value, (list, tuple)): for _id in value: - meth(_id) + meth(_id, ignore_unknown=True) else: meth(value) elif optname == 'output-format': @@ -622,7 +623,7 @@ warning, statement which respectively contain the number of errors / warnings\ self._ignore_file = False # fix the current file (if the source file was not available or # if it's actually a c extension) - self.current_file = astroid.file + self.current_file = astroid.file # pylint: disable=maybe-no-member self.check_astroid_module(astroid, walker, rawcheckers, tokencheckers) self._add_suppression_messages() # notify global end @@ -873,15 +874,20 @@ def preprocess_options(args, search_for): option, val = arg[2:], None try: cb, takearg = search_for[option] + except KeyError: + i += 1 + else: del args[i] if takearg and val is None: if i >= len(args) or args[i].startswith('-'): - raise ArgumentPreprocessingError(arg) + msg = 'Option %s expects a value' % option + raise ArgumentPreprocessingError(msg) val = args[i] del args[i] + elif not takearg and val is not None: + msg = "Option %s doesn't expects a value" % option + raise ArgumentPreprocessingError(msg) cb(option, val) - except KeyError: - i += 1 else: i += 1 @@ -901,12 +907,13 @@ group are mutually exclusive.'), self._plugins = [] try: preprocess_options(args, { - # option: (callback, takearg) - 'rcfile': (self.cb_set_rcfile, True), - 'load-plugins': (self.cb_add_plugins, True), - }) + # option: (callback, takearg) + 'init-hooks': (cb_init_hook, True), + 'rcfile': (self.cb_set_rcfile, True), + 'load-plugins': (self.cb_add_plugins, True), + }) except ArgumentPreprocessingError, ex: - print >> sys.stderr, 'Argument %s expects a value.' % (ex.args[0],) + print >> sys.stderr, ex sys.exit(32) self.linter = linter = self.LinterClass(( @@ -916,8 +923,9 @@ group are mutually exclusive.'), 'help' : 'Specify a configuration file.'}), ('init-hook', - {'action' : 'callback', 'type' : 'string', 'metavar': '<code>', - 'callback' : cb_init_hook, 'level': 1, + {'action' : 'callback', 'callback' : lambda *args: 1, + 'type' : 'string', 'metavar': '<code>', + 'level': 1, 'help' : 'Python code to execute, usually for sys.path \ manipulation such as pygtk.require().'}), @@ -1043,11 +1051,11 @@ are done by default'''}), sys.exit(self.linter.msg_status) def cb_set_rcfile(self, name, value): - """callback for option preprocessing (i.e. before optik parsing)""" + """callback for option preprocessing (i.e. before option parsing)""" self._rcfile = value def cb_add_plugins(self, name, value): - """callback for option preprocessing (i.e. before optik parsing)""" + """callback for option preprocessing (i.e. before option parsing)""" self._plugins.extend(splitstrip(value)) def cb_error_mode(self, *args, **kwargs): @@ -1086,7 +1094,7 @@ are done by default'''}), self.linter.list_messages() sys.exit(0) -def cb_init_hook(option, optname, value, parser): +def cb_init_hook(optname, value): """exec arbitrary code to set sys.path for instance""" exec value diff --git a/pyreverse/diadefslib.py b/pyreverse/diadefslib.py index 795be8b..46d0f19 100644 --- a/pyreverse/diadefslib.py +++ b/pyreverse/diadefslib.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """handle diagram generation options for class diagram or default diagrams """ diff --git a/pyreverse/diagrams.py b/pyreverse/diagrams.py index 360fdb1..6dde9a1 100644 --- a/pyreverse/diagrams.py +++ b/pyreverse/diagrams.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """diagram objects """ @@ -85,11 +85,11 @@ class ClassDiagram(Figure, FilterMixIn): if names: node_name = "%s : %s" % (node_name, ", ".join(names)) attrs.append(node_name) - return attrs + return sorted(attrs) def get_methods(self, node): """return visible methods""" - return [m for m in node.values() + return [m for m in sorted(node.values(), key=lambda n: n.name) if isinstance(m, astroid.Function) and self.show_attr(m.name)] def add_object(self, title, node): diff --git a/pyreverse/main.py b/pyreverse/main.py index 5b9e8df..7801835 100644 --- a/pyreverse/main.py +++ b/pyreverse/main.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ %prog [options] <packages> diff --git a/pyreverse/utils.py b/pyreverse/utils.py index ea90e05..3d12d41 100644 --- a/pyreverse/utils.py +++ b/pyreverse/utils.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ generic classes/functions for pyreverse core/extensions """ diff --git a/pyreverse/writer.py b/pyreverse/writer.py index d4b9937..6b35548 100644 --- a/pyreverse/writer.py +++ b/pyreverse/writer.py @@ -13,7 +13,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Utilities for creating VCG and Dot diagrams""" from logilab.common.vcgutils import VCGPrinter diff --git a/reporters/__init__.py b/reporters/__init__.py index 53064c7..a767a05 100644 --- a/reporters/__init__.py +++ b/reporters/__init__.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """utilities methods and classes for reporters""" import sys @@ -28,6 +28,10 @@ if sys.version_info >= (3, 0): def cmp(a, b): return (a > b) - (a < b) +if sys.version_info < (2, 6): + import stringformat + stringformat.init(True) + def diff_string(old, new): """given a old and new int value, return a string representing the difference diff --git a/reporters/html.py b/reporters/html.py index a51e0e7..71d46eb 100644 --- a/reporters/html.py +++ b/reporters/html.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """HTML reporter""" import sys diff --git a/reporters/text.py b/reporters/text.py index 614fcdb..bd99837 100644 --- a/reporters/text.py +++ b/reporters/text.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Plain text reporters: :text: the default one grouping messages by module @@ -52,7 +52,7 @@ sys.modules.pop('__pkginfo__', None) __pkginfo__ = __import__("__pkginfo__") # import required features from __pkginfo__ import modname, version, license, description, \ - web, author, author_email + web, author, author_email, classifiers distname = getattr(__pkginfo__, 'distname', modname) scripts = getattr(__pkginfo__, 'scripts', []) @@ -131,7 +131,15 @@ class MyInstallLib(install_lib.install_lib): else: exclude = set() shutil.rmtree(dest, ignore_errors=True) - shutil.copytree(directory, dest, ignore=lambda dir, names: list(set(names) & exclude)) + shutil.copytree(directory, dest) + # since python2.5's copytree doesn't support the ignore + # parameter, the following loop to remove the exclude set + # was added + for (dirpath, dirnames, filenames) in os.walk(dest): + for n in filenames: + if n in exclude: + os.remove(os.path.join(dirpath, n)) + if sys.version_info >= (3, 0): # process manually python file in include_dirs (test data) @@ -179,6 +187,7 @@ def install(**kwargs): author_email=author_email, url=web, scripts=ensure_scripts(scripts), + classifiers=classifiers, data_files=data_files, ext_modules=ext_modules, cmdclass={'install_lib': MyInstallLib, diff --git a/test/data/classes_No_Name.dot b/test/data/classes_No_Name.dot index e75553e..51b42e7 100644 --- a/test/data/classes_No_Name.dot +++ b/test/data/classes_No_Name.dot @@ -1,10 +1,10 @@ digraph "classes_No_Name" { charset="utf-8" rankdir=BT -"0" [label="{Ancestor|attr : str\lcls_member\l|set_value()\lget_value()\l}", shape="record"]; +"0" [label="{Ancestor|attr : str\lcls_member\l|get_value()\lset_value()\l}", shape="record"]; "1" [label="{DoNothing|\l|}", shape="record"]; "2" [label="{«interface»\nInterface|\l|get_value()\lset_value()\l}", shape="record"]; -"3" [label="{Specialization|relation\ltop : str\lTYPE : str\l|}", shape="record"]; +"3" [label="{Specialization|TYPE : str\lrelation\ltop : str\l|}", shape="record"]; "3" -> "0" [arrowhead="empty", arrowtail="none"]; "0" -> "2" [arrowhead="empty", arrowtail="node", style="dashed"]; "1" -> "0" [arrowhead="diamond", arrowtail="none", fontcolor="green", label="cls_member", style="solid"]; diff --git a/test/input/func_abstract_class_instantiated_py30.py b/test/input/func_abstract_class_instantiated_py30.py new file mode 100644 index 0000000..4bcfd3f --- /dev/null +++ b/test/input/func_abstract_class_instantiated_py30.py @@ -0,0 +1,43 @@ +"""Check that instantiating a class with +`abc.ABCMeta` as metaclass fails if it defines +abstract methods. +""" + +# pylint: disable=too-few-public-methods, missing-docstring, abstract-class-not-used + +__revision__ = 0 + +import abc +
+class GoodClass(object, metaclass=abc.ABCMeta):
+ pass
+
+class SecondGoodClass(object, metaclass=abc.ABCMeta):
+ def test(self):
+ """ do nothing. """
+ pass
+
+class ThirdGoodClass(object, metaclass=abc.ABCMeta):
+ """ This should not raise the warning. """
+ def test(self):
+ raise NotImplementedError()
+
+class BadClass(object, metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def test(self):
+ """ do nothing. """
+ pass
+
+class SecondBadClass(object, metaclass=abc.ABCMeta):
+ @property
+ @abc.abstractmethod
+ def test(self):
+ """ do nothing. """
+
+def main():
+ """ do nothing """
+ GoodClass()
+ SecondGoodClass()
+ ThirdGoodClass()
+ BadClass()
+ SecondBadClass()
diff --git a/test/input/func_abstract_class_instantiated_py34.py b/test/input/func_abstract_class_instantiated_py34.py new file mode 100644 index 0000000..1814f34 --- /dev/null +++ b/test/input/func_abstract_class_instantiated_py34.py @@ -0,0 +1,55 @@ +"""Check that instantiating a class with +`abc.ABCMeta` as metaclass fails if it defines +abstract methods. +""" + +# pylint: disable=too-few-public-methods, missing-docstring, abstract-class-not-used, no-init + +__revision__ = 0 + +import abc +
+class GoodClass(object, metaclass=abc.ABCMeta):
+ pass
+
+class SecondGoodClass(object, metaclass=abc.ABCMeta):
+ def test(self):
+ """ do nothing. """
+ pass
+
+class ThirdGoodClass(object, metaclass=abc.ABCMeta):
+ """ This should not raise the warning. """
+ def test(self):
+ raise NotImplementedError()
+
+class FourthGoodClass(abc.ABC):
+ """ Neither this. """
+ def test(self):
+ pass
+
+class BadClass(object, metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def test(self):
+ """ do nothing. """
+ pass
+
+class SecondBadClass(object, metaclass=abc.ABCMeta):
+ @property
+ @abc.abstractmethod
+ def test(self):
+ """ do nothing. """
+
+class ThirdBadClass(abc.ABC):
+ @abc.abstractmethod
+ def test(self):
+ pass
+
+def main():
+ """ do nothing """
+ GoodClass()
+ SecondGoodClass()
+ ThirdGoodClass()
+ FourthGoodClass()
+ BadClass()
+ SecondBadClass()
+ ThirdBadClass()
diff --git a/test/input/func_bad_slots.py b/test/input/func_bad_slots.py new file mode 100644 index 0000000..16f7a79 --- /dev/null +++ b/test/input/func_bad_slots.py @@ -0,0 +1,57 @@ +""" Checks that classes uses valid __slots__ """ + +# pylint: disable=too-few-public-methods, missing-docstring + +from collections import deque + +def func(): + if True: + return ("a", "b", "c") + else: + return [str(var) for var in range(3)] + +__revision__ = 0 + +class NotIterable(object): + def __iter_(self): + """ do nothing """ +
+class Good(object):
+ __slots__ = ()
+
+class SecondGood(object):
+ __slots__ = []
+
+class ThirdGood(object):
+ __slots__ = ['a']
+
+class FourthGood(object):
+ __slots__ = ('a%s' % i for i in range(10))
+
+class FifthGood(object):
+ __slots__ = "a"
+
+class SixthGood(object):
+ __slots__ = deque(["a", "b", "c"])
+
+class SeventhGood(object):
+ __slots__ = {"a": "b", "c": "d"}
+
+class Bad(object):
+ __slots__ = list
+
+class SecondBad(object):
+ __slots__ = 1
+
+class ThirdBad(object):
+ __slots__ = ('a', 2)
+
+class FourthBad(object):
+ __slots__ = NotIterable()
+
+class FifthBad(object):
+ __slots__ = ("a", "b", "")
+
+class PotentiallyGood(object):
+ __slots__ = func()
+
\ No newline at end of file diff --git a/test/input/func_class_access_protected_members.py b/test/input/func_class_access_protected_members.py index 123c5ef..f2ad5b7 100644 --- a/test/input/func_class_access_protected_members.py +++ b/test/input/func_class_access_protected_members.py @@ -26,7 +26,7 @@ class Subclass(MyClass): def __init__(self): MyClass._protected = 5 -INST = MyClass() +INST = Subclass() INST.attr = 1 print INST.attr INST._protected = 2 diff --git a/test/input/func_ctor_arguments.py b/test/input/func_ctor_arguments.py new file mode 100644 index 0000000..987a930 --- /dev/null +++ b/test/input/func_ctor_arguments.py @@ -0,0 +1,63 @@ +"""Test function argument checker on __init__ + +Based on test/input/func_arguments.py +""" +# pylint: disable=C0111,R0903,W0231 +__revision__ = '' + +class Class1Arg(object): + def __init__(self, first_argument): + """one argument function""" + +class Class3Arg(object): + def __init__(self, first_argument, second_argument, third_argument): + """three arguments function""" + +class ClassDefaultArg(object): + def __init__(self, one=1, two=2): + """function with default value""" + +class Subclass1Arg(Class1Arg): + pass + +class ClassAllArgs(Class1Arg): + def __init__(self, *args, **kwargs): + pass + +class ClassMultiInheritance(Class1Arg, Class3Arg): + pass + +class ClassNew(object): + def __new__(cls, first_argument, kwarg=None): + return first_argument, kwarg + +Class1Arg(420) +Class1Arg() +Class1Arg(1337, 347) + +Class3Arg(420, 789) +Class3Arg() +Class3Arg(1337, 347, 456) +Class3Arg('bab', 'bebe', None, 5.6) + +ClassDefaultArg(1, two=5) +ClassDefaultArg(two=5) + +Class1Arg(bob=4) +ClassDefaultArg(1, 4, coin="hello") + +ClassDefaultArg(1, one=5) + +Subclass1Arg(420) +Subclass1Arg() +Subclass1Arg(1337, 347) + +ClassAllArgs() +ClassAllArgs(1, 2, 3, even=4, more=5) + +ClassMultiInheritance(1) +ClassMultiInheritance(1, 2, 3) + +ClassNew(1, kwarg=1) +ClassNew(1, 2, 3) +ClassNew(one=2) diff --git a/test/input/func_docstring.py b/test/input/func_docstring.py index c7930f2..e73d8a3 100644 --- a/test/input/func_docstring.py +++ b/test/input/func_docstring.py @@ -2,6 +2,9 @@ __revision__ = '' +def function0(): + """""" + def function1(value): # missing docstring print value @@ -37,6 +40,10 @@ class AAAA(object): """ yeah !""" pass + def method3(self): + """""" + pass + def __init__(self): pass @@ -46,6 +53,21 @@ class DDDD(AAAA): def __init__(self): AAAA.__init__(self) + def method2(self): + """""" + pass + + def method3(self): + pass + + def method4(self): + pass + # pylint: disable=missing-docstring def function4(): pass + +# pylint: disable=empty-docstring +def function5(): + """""" + pass diff --git a/test/input/func_eval_used.py b/test/input/func_eval_used.py new file mode 100644 index 0000000..c58b69c --- /dev/null +++ b/test/input/func_eval_used.py @@ -0,0 +1,13 @@ +"""test for eval usage""" + +__revision__ = 0 + +eval('os.listdir(".")') +eval('os.listdir(".")', globals={}) + +eval('os.listdir(".")', globals=globals()) + +def func(): + """ eval in local scope""" + eval('b = 1') + diff --git a/test/input/func_f0401.py b/test/input/func_f0401.py index f8443fa..c663431 100644 --- a/test/input/func_f0401.py +++ b/test/input/func_f0401.py @@ -1,4 +1,4 @@ -"""tset failed import +"""test failed import """ __revision__ = 0 diff --git a/test/input/func_name_checking.py b/test/input/func_name_checking.py index ce7f439..d19d946 100644 --- a/test/input/func_name_checking.py +++ b/test/input/func_name_checking.py @@ -21,7 +21,7 @@ def run(): def __init__(self): pass bbb = 1 - return Aaa(bbb) + return bbb, Aaa A = None diff --git a/test/input/func_newstyle_super.py b/test/input/func_newstyle_super.py index fdba69c..a0aaa0a 100644 --- a/test/input/func_newstyle_super.py +++ b/test/input/func_newstyle_super.py @@ -1,5 +1,7 @@ -# pylint: disable=R0903 +# pylint: disable=R0903,import-error """check use of super""" + +from unknown import Missing __revision__ = None class Aaaa: @@ -29,3 +31,8 @@ class Py3kWrongSuper(Py3kAaaa): """new style""" def __init__(self): super(NewAaaa, self).__init__() + +class WrongNameRegression(Py3kAaaa): + """ test a regression with the message """ + def __init__(self): + super(Missing, self).__init__() diff --git a/test/input/func_noerror_abstract_method.py b/test/input/func_noerror_abstract_method.py new file mode 100644 index 0000000..18228c6 --- /dev/null +++ b/test/input/func_noerror_abstract_method.py @@ -0,0 +1,20 @@ +""" This should not warn about `prop` being abstract in Child """
+
+# pylint: disable=too-few-public-methods,abstract-class-little-used
+
+__revision__ = None
+
+import abc
+
+class Parent(object):
+ """ Class """
+ __metaclass__ = abc.ABCMeta
+
+ @property
+ @abc.abstractmethod
+ def prop(self):
+ """ Abstract """
+
+class Child(Parent):
+ """ No warning for the following. """
+ prop = property(lambda self: 1)
diff --git a/test/input/func_noerror_abstract_method_py30.py b/test/input/func_noerror_abstract_method_py30.py new file mode 100644 index 0000000..c237cb4 --- /dev/null +++ b/test/input/func_noerror_abstract_method_py30.py @@ -0,0 +1,19 @@ +""" This should not warn about `prop` being abstract in Child """
+
+# pylint: disable=too-few-public-methods,abstract-class-little-used,no-init,old-style-class
+
+__revision__ = None
+
+import abc
+
+class Parent(metaclass=abc.ABCMeta):
+ """ Class """
+
+ @property
+ @abc.abstractmethod
+ def prop(self):
+ """ Abstract """
+
+class Child(Parent):
+ """ No warning for the following. """
+ prop = property(lambda self: 1)
diff --git a/test/input/func_return_yield_mix.py b/test/input/func_return_yield_mix_py_33.py index 1a3cd5d..1a3cd5d 100644 --- a/test/input/func_return_yield_mix.py +++ b/test/input/func_return_yield_mix_py_33.py diff --git a/test/input/func_unpack_exception_py_30.py b/test/input/func_unpack_exception_py_30.py index cf2e237..356915e 100644 --- a/test/input/func_unpack_exception_py_30.py +++ b/test/input/func_unpack_exception_py_30.py @@ -6,8 +6,8 @@ def new_style(): """Some exceptions can be unpacked.""" try: pass - except IOError as (errno, message): # this is fine + except IOError, (errno, message): # this is fine print errno, message - except IOError as (new_style, tuple): # W0623 twice + except IOError, (new_style, tuple): # W0623 twice print new_style, tuple diff --git a/test/input/func_used_before_assignment_py30.py b/test/input/func_used_before_assignment_py30.py new file mode 100644 index 0000000..b5d0bf3 --- /dev/null +++ b/test/input/func_used_before_assignment_py30.py @@ -0,0 +1,28 @@ +"""Check for nonlocal and used-before-assignment"""
+# pylint: disable=missing-docstring, unused-variable
+
+__revision__ = 0
+
+def test_ok():
+ """ uses nonlocal """
+ cnt = 1
+ def wrap():
+ nonlocal cnt
+ cnt = cnt + 1
+ wrap()
+
+def test_fail():
+ """ doesn't use nonlocal """
+ cnt = 1
+ def wrap():
+ cnt = cnt + 1
+ wrap()
+
+def test_fail2():
+ """ use nonlocal, but for other variable """
+ cnt = 1
+ count = 1
+ def wrap():
+ nonlocal count
+ cnt = cnt + 1
+ wrap()
diff --git a/test/input/func_w0109.py b/test/input/func_w0109.py deleted file mode 100644 index ba7a679..0000000 --- a/test/input/func_w0109.py +++ /dev/null @@ -1,7 +0,0 @@ -"""test empty docstrings -""" - -__revision__ = '' - -def function(): - """""" diff --git a/test/input/func_w0401_package/__init__.py b/test/input/func_w0401_package/__init__.py new file mode 100644 index 0000000..dedef66 --- /dev/null +++ b/test/input/func_w0401_package/__init__.py @@ -0,0 +1,2 @@ +"""Our big package.""" +__revision__ = None diff --git a/test/input/func_w0401_package/all_the_things.py b/test/input/func_w0401_package/all_the_things.py new file mode 100644 index 0000000..ef0ca99 --- /dev/null +++ b/test/input/func_w0401_package/all_the_things.py @@ -0,0 +1,8 @@ +"""All the things!""" +__revision__ = None +from .thing1 import THING1 +from .thing2 import THING2 +from .thing2 import THING1_PLUS_THING2 + +_ = (THING1, THING2, THING1_PLUS_THING2) +del _ diff --git a/test/input/func_w0401_package/thing1.py b/test/input/func_w0401_package/thing1.py new file mode 100644 index 0000000..34972a7 --- /dev/null +++ b/test/input/func_w0401_package/thing1.py @@ -0,0 +1,3 @@ +"""The first thing.""" +__revision__ = None +THING1 = "I am thing1" diff --git a/test/input/func_w0401_package/thing2.py b/test/input/func_w0401_package/thing2.py new file mode 100644 index 0000000..836cd9c --- /dev/null +++ b/test/input/func_w0401_package/thing2.py @@ -0,0 +1,7 @@ +"""The second thing.""" +__revision__ = None +from .all_the_things import THING1 + +THING2 = "I am thing2" +THING1_PLUS_THING2 = "%s, plus %s" % (THING1, THING2) + diff --git a/test/input/func_w0402.py b/test/input/func_w0402.py index 53752db..d2707ee 100644 --- a/test/input/func_w0402.py +++ b/test/input/func_w0402.py @@ -3,6 +3,9 @@ __revision__ = 0 from input.func_fixme import * +# This is an unresolved import which still generates the wildcard-import +# warning. +from unknown.package import * def abcd(): """use imports""" diff --git a/test/input/func_w0613.py b/test/input/func_w0613.py index 4d51d10..1df71dd 100644 --- a/test/input/func_w0613.py +++ b/test/input/func_w0613.py @@ -14,7 +14,7 @@ class AAAA(object): def method(self, arg): """dummy method""" print self - def __init__(self): + def __init__(self, *unused_args, **unused_kwargs): pass @classmethod diff --git a/test/input/func_w0623_py30.py b/test/input/func_w0623_py30.py index 163256b..f6c5f57 100644 --- a/test/input/func_w0623_py30.py +++ b/test/input/func_w0623_py30.py @@ -12,5 +12,5 @@ def some_function(): try: {}["a"] - except KeyError as some_function: # W0623 + except KeyError, some_function: # W0623 pass diff --git a/test/messages/func_abstract_class_instantiated_py30.txt b/test/messages/func_abstract_class_instantiated_py30.txt new file mode 100644 index 0000000..28dc11e --- /dev/null +++ b/test/messages/func_abstract_class_instantiated_py30.txt @@ -0,0 +1,2 @@ +E: 42:main: Abstract class with abstract methods instantiated
+E: 43:main: Abstract class with abstract methods instantiated
\ No newline at end of file diff --git a/test/messages/func_abstract_class_instantiated_py34.txt b/test/messages/func_abstract_class_instantiated_py34.txt new file mode 100644 index 0000000..310f8ea --- /dev/null +++ b/test/messages/func_abstract_class_instantiated_py34.txt @@ -0,0 +1,3 @@ +E: 53:main: Abstract class with abstract methods instantiated
+E: 54:main: Abstract class with abstract methods instantiated
+E: 55:main: Abstract class with abstract methods instantiated
\ No newline at end of file diff --git a/test/messages/func_arguments.txt b/test/messages/func_arguments.txt index 33dbf56..642d212 100644 --- a/test/messages/func_arguments.txt +++ b/test/messages/func_arguments.txt @@ -1,11 +1,11 @@ -E: 18: No value passed for parameter 'first_argument' in function call +E: 18: No value for argument 'first_argument' in function call E: 19: Too many positional arguments for function call -E: 21: No value passed for parameter 'third_argument' in function call -E: 22: No value passed for parameter 'first_argument' in function call -E: 22: No value passed for parameter 'second_argument' in function call -E: 22: No value passed for parameter 'third_argument' in function call +E: 21: No value for argument 'third_argument' in function call +E: 22: No value for argument 'first_argument' in function call +E: 22: No value for argument 'second_argument' in function call +E: 22: No value for argument 'third_argument' in function call E: 24: Too many positional arguments for function call -E: 31: No value passed for parameter 'first_argument' in function call -E: 31: Passing unexpected keyword argument 'bob' in function call -E: 32: Passing unexpected keyword argument 'coin' in function call -E: 34: Parameter 'one' passed as both positional and keyword argument +E: 31: No value for argument 'first_argument' in function call +E: 31: Unexpected keyword argument 'bob' in function call +E: 32: Unexpected keyword argument 'coin' in function call +E: 34: Argument 'one' passed by position and keyword in function call diff --git a/test/messages/func_bad_slots.txt b/test/messages/func_bad_slots.txt new file mode 100644 index 0000000..ec44646 --- /dev/null +++ b/test/messages/func_bad_slots.txt @@ -0,0 +1,4 @@ +E: 43:SecondBad: Invalid __slots__ object
+E: 47:ThirdBad: Invalid object '2' in __slots__, must contain only non empty strings
+E: 49:FourthBad: Invalid __slots__ object
+E: 53:FifthBad: Invalid object "''" in __slots__, must contain only non empty strings
\ No newline at end of file diff --git a/test/messages/func_ctor_arguments.txt b/test/messages/func_ctor_arguments.txt new file mode 100644 index 0000000..b8d62b1 --- /dev/null +++ b/test/messages/func_ctor_arguments.txt @@ -0,0 +1,17 @@ +E: 35: No value for argument 'first_argument' in constructor call +E: 36: Too many positional arguments for constructor call +E: 38: No value for argument 'third_argument' in constructor call +E: 39: No value for argument 'first_argument' in constructor call +E: 39: No value for argument 'second_argument' in constructor call +E: 39: No value for argument 'third_argument' in constructor call +E: 41: Too many positional arguments for constructor call +E: 46: No value for argument 'first_argument' in constructor call +E: 46: Unexpected keyword argument 'bob' in constructor call +E: 47: Unexpected keyword argument 'coin' in constructor call +E: 49: Argument 'one' passed by position and keyword in constructor call +E: 52: No value for argument 'first_argument' in constructor call +E: 53: Too many positional arguments for constructor call +E: 59: Too many positional arguments for constructor call +E: 62: Too many positional arguments for constructor call +E: 63: No value for argument 'first_argument' in constructor call +E: 63: Unexpected keyword argument 'one' in constructor call diff --git a/test/messages/func_docstring.txt b/test/messages/func_docstring.txt index 2b0b191..932df5e 100644 --- a/test/messages/func_docstring.txt +++ b/test/messages/func_docstring.txt @@ -1,4 +1,8 @@ C: 1: Missing module docstring -C: 5:function1: Missing function docstring -C: 17:AAAA: Missing class docstring -C: 33:AAAA.method1: Missing method docstring +C: 5:function0: Empty function docstring +C: 8:function1: Missing function docstring +C: 20:AAAA: Missing class docstring +C: 36:AAAA.method1: Missing method docstring +C: 43:AAAA.method3: Empty method docstring +C: 56:DDDD.method2: Empty method docstring +C: 63:DDDD.method4: Missing method docstring diff --git a/test/messages/func_e0205.txt b/test/messages/func_e0205.txt index 494f3c3..c7402ce 100644 --- a/test/messages/func_e0205.txt +++ b/test/messages/func_e0205.txt @@ -1,2 +1,2 @@ -E: 14:Cdef.abcd: An attribute affected in input.func_e0205 line 10 hide this method +E: 14:Cdef.abcd: An attribute defined in input.func_e0205 line 10 hides this method diff --git a/test/messages/func_eval_used.txt b/test/messages/func_eval_used.txt new file mode 100644 index 0000000..ab65307 --- /dev/null +++ b/test/messages/func_eval_used.txt @@ -0,0 +1,4 @@ +W: 5: Use of eval
+W: 6: Use of eval
+W: 8: Use of eval
+W: 12:func: Use of eval
\ No newline at end of file diff --git a/test/messages/func_kwoa_py30.txt b/test/messages/func_kwoa_py30.txt index 5ccdf00..08dd8c5 100644 --- a/test/messages/func_kwoa_py30.txt +++ b/test/messages/func_kwoa_py30.txt @@ -1,5 +1,5 @@ -E: 10: Missing mandatory keyword argument 'foo' +E: 10: Missing mandatory keyword argument 'foo' in function call E: 10: Too many positional arguments for function call -E: 12: Missing mandatory keyword argument 'foo' +E: 12: Missing mandatory keyword argument 'foo' in function call E: 12: Too many positional arguments for function call W: 3:function: Redefining name 'foo' from outer scope (line 9) diff --git a/test/messages/func_newstyle___slots___py30.txt b/test/messages/func_newstyle___slots___py30.txt new file mode 100644 index 0000000..0db581b --- /dev/null +++ b/test/messages/func_newstyle___slots___py30.txt @@ -0,0 +1 @@ +C: 10:HaNonNonNon: Old-style class defined. diff --git a/test/messages/func_newstyle_exceptions_py30.txt b/test/messages/func_newstyle_exceptions_py30.txt index 72166ba..ed4ba4d 100644 --- a/test/messages/func_newstyle_exceptions_py30.txt +++ b/test/messages/func_newstyle_exceptions_py30.txt @@ -1,5 +1,5 @@ +E: 21:fonctionBof: Raising a new style class which doesn't inherit from BaseException E: 25:fonctionNew: Raising a new style class which doesn't inherit from BaseException +E: 29:fonctionBof2: Raising a new style class which doesn't inherit from BaseException E: 33:fonctionNew2: Raising a new style class which doesn't inherit from BaseException E: 37:fonctionNotImplemented: NotImplemented raised - should raise NotImplementedError -W: 21:fonctionBof: Exception doesn't inherit from standard "Exception" class -W: 29:fonctionBof2: Exception doesn't inherit from standard "Exception" class diff --git a/test/messages/func_newstyle_property_py30.txt b/test/messages/func_newstyle_property_py30.txt new file mode 100644 index 0000000..b1d46a5 --- /dev/null +++ b/test/messages/func_newstyle_property_py30.txt @@ -0,0 +1 @@ +C: 14:HaNonNonNon: Old-style class defined. diff --git a/test/messages/func_newstyle_super.txt b/test/messages/func_newstyle_super.txt index a0192d7..b51869b 100644 --- a/test/messages/func_newstyle_super.txt +++ b/test/messages/func_newstyle_super.txt @@ -1,6 +1,7 @@ -C: 5:Aaaa: Old-style class defined. -E: 7:Aaaa.hop: Use of super on an old style class -E: 11:Aaaa.__init__: Use of super on an old style class -E: 21:NewAaaa.__init__: Bad first argument 'object' given to super() -E: 26:Py3kAaaa.__init__: Missing argument to super() -E: 31:Py3kWrongSuper.__init__: Bad first argument 'NewAaaa' given to super() +C: 7:Aaaa: Old-style class defined. +E: 9:Aaaa.hop: Use of super on an old style class +E: 13:Aaaa.__init__: Use of super on an old style class +E: 23:NewAaaa.__init__: Bad first argument 'object' given to super() +E: 28:Py3kAaaa.__init__: Missing argument to super() +E: 33:Py3kWrongSuper.__init__: Bad first argument 'NewAaaa' given to super() +E: 38:WrongNameRegression.__init__: Bad first argument 'Missing' given to super()
\ No newline at end of file diff --git a/test/messages/func_newstyle_super_py30.txt b/test/messages/func_newstyle_super_py30.txt index 4ed306c..246d265 100644 --- a/test/messages/func_newstyle_super_py30.txt +++ b/test/messages/func_newstyle_super_py30.txt @@ -1,5 +1,4 @@ -C: 5:Aaaa: Old-style class defined. -E: 7:Aaaa.hop: Use of super on an old style class -E: 11:Aaaa.__init__: Use of super on an old style class -E: 21:NewAaaa.__init__: Bad first argument 'object' given to super() -E: 31:Py3kWrongSuper.__init__: Bad first argument 'NewAaaa' given to super() +C: 7:Aaaa: Old-style class defined. +E: 23:NewAaaa.__init__: Bad first argument 'object' given to super() +E: 33:Py3kWrongSuper.__init__: Bad first argument 'NewAaaa' given to super() +E: 38:WrongNameRegression.__init__: Bad first argument 'Missing' given to super() diff --git a/test/messages/func_return_yield_mix.txt b/test/messages/func_return_yield_mix_py_33.txt index d81ce5c..d81ce5c 100644 --- a/test/messages/func_return_yield_mix.txt +++ b/test/messages/func_return_yield_mix_py_33.txt diff --git a/test/messages/func_unpacking_non_sequence.txt b/test/messages/func_unpacking_non_sequence.txt index 55a7745..4099e29 100644 --- a/test/messages/func_unpacking_non_sequence.txt +++ b/test/messages/func_unpacking_non_sequence.txt @@ -6,3 +6,4 @@ W: 65: Attempting to unpack a non-sequence defined at line 9 of input.unpacking W: 66: Attempting to unpack a non-sequence defined at line 11 of input.unpacking W: 67: Attempting to unpack a non-sequence defined at line 58 W: 68: Attempting to unpack a non-sequence + diff --git a/test/messages/func_used_before_assignment_py30.txt b/test/messages/func_used_before_assignment_py30.txt new file mode 100644 index 0000000..5b6080f --- /dev/null +++ b/test/messages/func_used_before_assignment_py30.txt @@ -0,0 +1,2 @@ +E: 18:test_fail.wrap: Using variable 'cnt' before assignment
+E: 27:test_fail2.wrap: Using variable 'cnt' before assignment
\ No newline at end of file diff --git a/test/messages/func_w0109.txt b/test/messages/func_w0109.txt deleted file mode 100644 index bcb97aa..0000000 --- a/test/messages/func_w0109.txt +++ /dev/null @@ -1 +0,0 @@ -C: 6:function: Empty function docstring diff --git a/test/messages/func_w0401_package.txt b/test/messages/func_w0401_package.txt new file mode 100644 index 0000000..4b1145b --- /dev/null +++ b/test/messages/func_w0401_package.txt @@ -0,0 +1 @@ +R: 1: Cyclic import (input.func_w0401_package.all_the_things -> input.func_w0401_package.thing2) diff --git a/test/messages/func_w0402.txt b/test/messages/func_w0402.txt index cf06fc4..453fc06 100644 --- a/test/messages/func_w0402.txt +++ b/test/messages/func_w0402.txt @@ -1,2 +1,3 @@ +F: 8: Unable to import 'unknown.package' W: 5: Wildcard import input.func_fixme - +W: 8: Wildcard import unknown.package diff --git a/test/regrtest_data/package_all/__init__.py b/test/regrtest_data/package_all/__init__.py new file mode 100644 index 0000000..4e3696b --- /dev/null +++ b/test/regrtest_data/package_all/__init__.py @@ -0,0 +1,3 @@ +""" Check for E0603 for missing submodule found in __all__ """
+__revision__ = 1
+__all__ = ['notmissing', 'missing']
diff --git a/test/regrtest_data/package_all/notmissing.py b/test/regrtest_data/package_all/notmissing.py new file mode 100644 index 0000000..7cf8543 --- /dev/null +++ b/test/regrtest_data/package_all/notmissing.py @@ -0,0 +1,2 @@ +""" empty """
+__revision__ = 1
diff --git a/test/smoketest.py b/test/smoketest.py index 25f30fd..26e2c9a 100644 --- a/test/smoketest.py +++ b/test/smoketest.py @@ -9,7 +9,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys from os.path import join, dirname, abspath @@ -39,7 +39,16 @@ class RunTC(TestCase): try: Run(args, reporter=reporter) except SystemExit, ex: - self.assertEqual(ex.code, code) + if reporter: + output = reporter.out.getvalue() + elif hasattr(out, 'getvalue'): + output = out.getvalue() + else: + output = None + msg = 'expected output status %s, got %s' % (code, ex.code) + if output is not None: + msg = '%s. Below pylint output: \n%s' % (msg, output) + self.assertEqual(ex.code, code, msg) else: self.fail('expected system exit') finally: diff --git a/test/test_base.py b/test/test_base.py index 9bd3aa5..972c783 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -4,46 +4,46 @@ import re from astroid import test_utils from pylint.checkers import base -from pylint.testutils import CheckerTestCase, Message +from pylint.testutils import CheckerTestCase, Message, set_config class DocstringTest(CheckerTestCase): CHECKER_CLASS = base.DocStringChecker - def testMissingDocstringModule(self): + def test_missing_docstring_module(self): module = test_utils.build_module("") with self.assertAddsMessages(Message('missing-docstring', node=module, args=('module',))): self.checker.visit_module(module) - def testEmptyDocstringModule(self): + def test_empty_docstring_module(self): module = test_utils.build_module("''''''") with self.assertAddsMessages(Message('empty-docstring', node=module, args=('module',))): self.checker.visit_module(module) - def testEmptyDocstringFunction(self): + def test_empty_docstring_function(self): func = test_utils.extract_node(""" def func(tion): pass""") with self.assertAddsMessages(Message('missing-docstring', node=func, args=('function',))): self.checker.visit_function(func) - def testShortFunctionNoDocstring(self): - self.checker.config.docstring_min_length = 2 + @set_config(docstring_min_length=2) + def test_short_function_no_docstring(self): func = test_utils.extract_node(""" def func(tion): pass""") with self.assertNoMessages(): self.checker.visit_function(func) - def testFunctionNoDocstringByName(self): - self.checker.config.docstring_min_length = 2 + @set_config(docstring_min_length=2) + def test_function_no_docstring_by_name(self): func = test_utils.extract_node(""" def __fun__(tion): pass""") with self.assertNoMessages(): self.checker.visit_function(func) - def testClassNoDocstring(self): + def test_class_no_docstring(self): klass = test_utils.extract_node(""" class Klass(object): pass""") @@ -57,11 +57,32 @@ class NameCheckerTest(CheckerTestCase): 'bad_names': set(), } - def testPropertyNames(self): + @set_config(include_naming_hint=True) + def test_naming_hint(self): + const = test_utils.extract_node(""" + const = "CONSTANT" #@ + """) + with self.assertAddsMessages( + Message('invalid-name', node=const.targets[0], + args=('constant', 'const', ' (hint: (([A-Z_][A-Z0-9_]*)|(__.*__))$)'))): + self.checker.visit_assname(const.targets[0]) + + @set_config(include_naming_hint=True, + const_name_hint='CONSTANT') + def test_naming_hint_configured_hint(self): + const = test_utils.extract_node(""" + const = "CONSTANT" #@ + """) + with self.assertAddsMessages( + Message('invalid-name', node=const.targets[0], + args=('constant', 'const', ' (hint: CONSTANT)'))): + self.checker.visit_assname(const.targets[0]) + + @set_config(attr_rgx=re.compile('[A-Z]+')) + def test_property_names(self): # If a method is annotated with @property, it's name should # match the attr regex. Since by default the attribute regex is the same # as the method regex, we override it here. - self.checker.config.attr_rgx = re.compile('[A-Z]+') methods = test_utils.extract_node(""" import abc @@ -82,11 +103,11 @@ class NameCheckerTest(CheckerTestCase): self.checker.visit_function(methods[0]) self.checker.visit_function(methods[2]) with self.assertAddsMessages(Message('invalid-name', node=methods[1], - args=('attribute', 'bar'))): + args=('attribute', 'bar', ''))): self.checker.visit_function(methods[1]) - def testPropertySetters(self): - self.checker.config.attr_rgx = re.compile('[A-Z]+') + @set_config(attr_rgx=re.compile('[A-Z]+')) + def test_property_setters(self): method = test_utils.extract_node(""" class FooClass(object): @property @@ -99,7 +120,7 @@ class NameCheckerTest(CheckerTestCase): with self.assertNoMessages(): self.checker.visit_function(method) - def testModuleLevelNames(self): + def test_module_level_names(self): assign = test_utils.extract_node(""" import collections Class = collections.namedtuple("a", ("b", "c")) #@ @@ -130,6 +151,73 @@ class NameCheckerTest(CheckerTestCase): self.checker.visit_assname(assign.targets[0]) +class MultiNamingStyleTest(CheckerTestCase): + CHECKER_CLASS = base.NameChecker + + MULTI_STYLE_RE = re.compile('(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$') + + @set_config(class_rgx=MULTI_STYLE_RE) + def test_multi_name_detection_first(self): + classes = test_utils.extract_node(""" + class CLASSA(object): #@ + pass + class classb(object): #@ + pass + class CLASSC(object): #@ + pass + """) + with self.assertAddsMessages(Message('invalid-name', node=classes[1], args=('class', 'classb', ''))): + for cls in classes: + self.checker.visit_class(cls) + + @set_config(class_rgx=MULTI_STYLE_RE) + def test_multi_name_detection_first_invalid(self): + classes = test_utils.extract_node(""" + class class_a(object): #@ + pass + class classb(object): #@ + pass + class CLASSC(object): #@ + pass + """) + with self.assertAddsMessages(Message('invalid-name', node=classes[0], args=('class', 'class_a', '')), + Message('invalid-name', node=classes[2], args=('class', 'CLASSC', ''))): + for cls in classes: + self.checker.visit_class(cls) + + @set_config(method_rgx=MULTI_STYLE_RE, + function_rgx=MULTI_STYLE_RE, + name_group=('function:method',)) + def test_multi_name_detection_group(self): + function_defs = test_utils.extract_node(""" + class First(object): + def func(self): #@ + pass + + def FUNC(): #@ + pass + """, module_name='test') + with self.assertAddsMessages(Message('invalid-name', node=function_defs[1], args=('function', 'FUNC', ''))): + for func in function_defs: + self.checker.visit_function(func) + + @set_config(function_rgx=re.compile('(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$')) + def test_multi_name_detection_exempt(self): + function_defs = test_utils.extract_node(""" + def FOO(): #@ + pass + def lower(): #@ + pass + def FOO(): #@ + pass + def UPPER(): #@ + pass + """) + with self.assertAddsMessages(Message('invalid-name', node=function_defs[3], args=('function', 'UPPER', ''))): + for func in function_defs: + self.checker.visit_function(func) + + if __name__ == '__main__': from logilab.common.testlib import unittest_main unittest_main() diff --git a/test/test_format.py b/test/test_format.py index b682671..9494f75 100644 --- a/test/test_format.py +++ b/test/test_format.py @@ -9,7 +9,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Copyright (c) 2000-2011 LOGILAB S.A. (Paris, FRANCE). http://www.logilab.fr/ -- mailto:contact@logilab.fr @@ -27,7 +27,7 @@ from astroid import test_utils from pylint.checkers.format import * -from pylint.testutils import CheckerTestCase, Message +from pylint.testutils import CheckerTestCase, Message, set_config def tokenize_str(code): @@ -171,8 +171,8 @@ class CheckSpaceTest(CheckerTestCase): with self.assertNoMessages(): self.checker.process_tokens(tokenize_str('(a,)\n')) + @set_config(no_space_check=[]) def testTrailingCommaBad(self): - self.checker.config.no_space_check = [] with self.assertAddsMessages( Message('C0326', line=1, args=('No', 'allowed', 'before', 'bracket', '(a, )\n ^'))): diff --git a/test/test_func.py b/test/test_func.py index e8c746d..807878a 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """functional/non regression tests for pylint""" import unittest @@ -25,7 +25,7 @@ from os.path import abspath, dirname, join from logilab.common import testlib from pylint.testutils import (make_tests, LintTestUsingModule, LintTestUsingFile, - cb_test_gen, linter, test_reporter) + LintTestUpdate, cb_test_gen, linter, test_reporter) PY3K = sys.version_info >= (3, 0) @@ -55,12 +55,16 @@ class TestTests(testlib.TestCase): continue todo.sort() if PY3K: - rest = ['I0001', + rest = ['E1001', # slots-on-old-class + 'E1002', # super-on-old-class # XXX : no use case for now : + 'I0001', 'W0402', # deprecated module 'W0403', # implicit relative import 'W0410', # __future__ import not first statement - ] + 'W0710', # nonstandard-exception + 'W1001' # property-on-old-class + ] self.assertEqual(todo, rest) else: self.assertEqual(todo, ['I0001']) @@ -71,24 +75,19 @@ class LintBuiltinModuleTest(LintTestUsingModule): def test_functionality(self): self._test(['sys']) -# Callbacks - -base_cb_file = cb_test_gen(LintTestUsingFile) - -def cb_file(*args): - if MODULES_ONLY: - return None - else: - return base_cb_file(*args) - -callbacks = [cb_test_gen(LintTestUsingModule), - cb_file] - -# Gen tests def gen_tests(filter_rgx): + if UPDATE: + callbacks = [cb_test_gen(LintTestUpdate)] + else: + callbacks = [cb_test_gen(LintTestUsingModule)] + if not MODULES_ONLY: + callbacks.append(cb_test_gen(LintTestUsingFile)) tests = make_tests(INPUT_DIR, MSG_DIR, filter_rgx, callbacks) + if UPDATE: + return tests + if filter_rgx: is_to_run = re.compile(filter_rgx).search else: @@ -109,19 +108,22 @@ def gen_tests(filter_rgx): FILTER_RGX = None MODULES_ONLY = False +UPDATE = False def suite(): return testlib.TestSuite([unittest.makeSuite(test, suiteClass=testlib.TestSuite) for test in gen_tests(FILTER_RGX)]) -del LintTestUsingModule -del LintTestUsingFile if __name__=='__main__': if '-m' in sys.argv: MODULES_ONLY = True sys.argv.remove('-m') + if '-u' in sys.argv: + UPDATE = True + sys.argv.remove('-u') + if len(sys.argv) > 1: FILTER_RGX = sys.argv[1] del sys.argv[1] diff --git a/test/test_logging.py b/test/test_logging.py new file mode 100644 index 0000000..fe7e638 --- /dev/null +++ b/test/test_logging.py @@ -0,0 +1,49 @@ +# Copyright 2014 Google Inc. All Rights Reserved. +""" +Unittest for the logging checker. +""" +from logilab.common.testlib import unittest_main +from astroid import test_utils + +from pylint.checkers import logging + +from pylint.testutils import CheckerTestCase, Message, set_config + + +class LoggingModuleDetectionTest(CheckerTestCase): + CHECKER_CLASS = logging.LoggingChecker + + def test_detects_standard_logging_module(self): + stmts = test_utils.extract_node(""" + import logging #@ + logging.warn('%s' % '%s') #@ + """) + self.checker.visit_module(None) + self.checker.visit_import(stmts[0]) + with self.assertAddsMessages(Message('W1201', node=stmts[1])): + self.checker.visit_callfunc(stmts[1]) + + def test_detects_renamed_standard_logging_module(self): + stmts = test_utils.extract_node(""" + import logging as blogging #@ + blogging.warn('%s' % '%s') #@ + """) + self.checker.visit_module(None) + self.checker.visit_import(stmts[0]) + with self.assertAddsMessages(Message('W1201', node=stmts[1])): + self.checker.visit_callfunc(stmts[1]) + + @set_config(logging_modules=['logging', 'my.logging']) + def test_nonstandard_logging_module(self): + stmts = test_utils.extract_node(""" + from my import logging as blogging #@ + blogging.warn('%s' % '%s') #@ + """) + self.checker.visit_module(None) + self.checker.visit_import(stmts[0]) + with self.assertAddsMessages(Message('W1201', node=stmts[1])): + self.checker.visit_callfunc(stmts[1]) + + +if __name__ == '__main__': + unittest_main() diff --git a/test/test_misc.py b/test/test_misc.py index a2cba9b..5a10936 100644 --- a/test/test_misc.py +++ b/test/test_misc.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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ Tests for the misc checker. """ @@ -22,8 +22,9 @@ import contextlib from logilab.common.testlib import unittest_main from astroid import test_utils -from pylint.checkers import misc -from pylint.testutils import CheckerTestCase, Message +from pylint.checkers import misc, variables +from pylint.testutils import CheckerTestCase, Message, linter, set_config + @contextlib.contextmanager def create_file_backed_module(code): @@ -57,8 +58,8 @@ class FixmeTest(CheckerTestCase): Message(msg_id='W0511', line=2, args=u'FIXME')): self.checker.process_module(module) + @set_config(notes=[]) def test_empty_fixme_regex(self): - self.checker.config.notes = [] with create_file_backed_module( """a = 1 # fixme @@ -66,6 +67,20 @@ class FixmeTest(CheckerTestCase): with self.assertNoMessages(): self.checker.process_module(module) +class MissingSubmoduleTest(CheckerTestCase): + CHECKER_CLASS = variables.VariablesChecker + + def test_package_all(self): + regr_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'regrtest_data') + sys.path.insert(0, regr_data) + try: + linter.check(os.path.join(regr_data, 'package_all')) + got = linter.reporter.finalize().strip() + self.assertEqual(got, "E: 3: Undefined variable name " + "'missing' in __all__") + finally: + sys.path.pop(0) if __name__ == '__main__': unittest_main() diff --git a/test/test_regr.py b/test/test_regr.py index 93aef06..0349481 100644 --- a/test/test_regr.py +++ b/test/test_regr.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """non regression tests for pylint, which requires a too specific configuration to be incorporated in the automatic functional test framework """ diff --git a/test/test_utils.py b/test/test_utils.py index fc35c3e..d8c4c67 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. from logilab.common.testlib import TestCase from astroid import test_utils diff --git a/test/unittest_checkers_utils.py b/test/unittest_checkers_utils.py index f7b0e80..d8ebd3b 100644 --- a/test/unittest_checkers_utils.py +++ b/test/unittest_checkers_utils.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """test the pylint.checkers.utils module """ diff --git a/test/unittest_lint.py b/test/unittest_lint.py index 44278e2..a7213bd 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2014 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 @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import sys import os @@ -49,23 +49,6 @@ class GetNoteMessageTC(TestCase): HERE = abspath(dirname(__file__)) INPUTDIR = join(HERE, 'input') -class RunTC(TestCase): - - def _test_run(self, args, exit_code=1, no_exit_fail=True): - sys.stdout = sys.sterr = StringIO() - try: - try: - Run(args) - except SystemExit, ex: - print sys.stdout.getvalue() - self.assertEqual(ex.code, exit_code) - else: - if no_exit_fail: - self.fail() - finally: - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - class PyLinterTC(TestCase): @@ -82,7 +65,7 @@ class PyLinterTC(TestCase): # logilab.common.textutils.normalize_text # uses os.linesep, which will # not properly compare with triple - # quoted multilines used in these tests + # quoted multilines used in these tests self.assertMultiLineEqual(desc, msg.format_help(checkerref=checkerref) .replace('\r\n', '\n')) @@ -111,13 +94,13 @@ class PyLinterTC(TestCase): msg = build_message_def(self.linter._checkers['typecheck'][0], 'E1122', checkers.typecheck.MSGS['E1122']) self._compare_messages( - ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in function call* + ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in %s call* Used when a function call passes the same keyword argument multiple times. This message belongs to the typecheck checker. It can't be emitted when using Python >= 2.6.''', msg, checkerref=True) self._compare_messages( - ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in function call* + ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in %s call* Used when a function call passes the same keyword argument multiple times. This message can't be emitted when using Python >= 2.6.''', msg, checkerref=False) @@ -381,9 +364,9 @@ class PyLinterTC(TestCase): def test_add_renamed_message(self): self.linter.add_renamed_message('C9999', 'old-bad-name', 'invalid-name') - self.assertEqual('invalid-name', + self.assertEqual('invalid-name', self.linter.check_message_id('C9999').symbol) - self.assertEqual('invalid-name', + self.assertEqual('invalid-name', self.linter.check_message_id('old-bad-name').symbol) def test_renamed_message_register(self): @@ -391,11 +374,16 @@ class PyLinterTC(TestCase): msgs = {'W1234': ('message', 'msg-symbol', 'msg-description', {'old_names': [('W0001', 'old-symbol')]})} self.linter.register_messages(Checker()) - self.assertEqual('msg-symbol', + self.assertEqual('msg-symbol', self.linter.check_message_id('W0001').symbol) - self.assertEqual('msg-symbol', + self.assertEqual('msg-symbol', self.linter.check_message_id('old-symbol').symbol) - + + def test_init_hooks_called_before_load_plugins(self): + self.assertRaises(RuntimeError, + Run, ['--load-plugins', 'unexistant', '--init-hooks', 'raise RuntimeError']) + self.assertRaises(RuntimeError, + Run, ['--init-hooks', 'raise RuntimeError', '--load-plugins', 'unexistant']) class ConfigTC(TestCase): @@ -473,7 +461,6 @@ class ConfigTC(TestCase): os.chdir(HERE) rmtree(chroot) - def test_pylintrc_parentdir_no_package(self): chroot = tempfile.mkdtemp() @@ -509,7 +496,7 @@ class PreprocessOptionsTC(TestCase): def _callback(self, name, value): self.args.append((name, value)) - def test_preprocess(self): + def test_value_equal(self): self.args = [] preprocess_options(['--foo', '--bar=baz', '--qu=ux'], {'foo' : (self._callback, False), @@ -517,7 +504,14 @@ class PreprocessOptionsTC(TestCase): self.assertEqual( [('foo', None), ('qu', 'ux')], self.args) - def test_preprocessing_error(self): + def test_value_space(self): + self.args = [] + preprocess_options(['--qu', 'ux'], + {'qu' : (self._callback, True)}) + self.assertEqual( + [('qu', 'ux')], self.args) + + def test_error_missing_expected_value(self): self.assertRaises( ArgumentPreprocessingError, preprocess_options, @@ -529,6 +523,13 @@ class PreprocessOptionsTC(TestCase): ['--foo', '--bar'], {'bar' : (None, True)}) + def test_error_unexpected_value(self): + self.assertRaises( + ArgumentPreprocessingError, + preprocess_options, + ['--foo', '--bar=spam', '--qu=ux'], + {'bar' : (None, False)}) + if __name__ == '__main__': unittest_main() diff --git a/test/unittest_pyreverse_diadefs.py b/test/unittest_pyreverse_diadefs.py index a42d73a..0f67e36 100644 --- a/test/unittest_pyreverse_diadefs.py +++ b/test/unittest_pyreverse_diadefs.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ unittest for the extensions.diadefslib modules """ diff --git a/test/unittest_pyreverse_writer.py b/test/unittest_pyreverse_writer.py index b850679..19d94ab 100644 --- a/test/unittest_pyreverse_writer.py +++ b/test/unittest_pyreverse_writer.py @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ unittest for visitors.diadefs and extensions.diadefslib modules """ diff --git a/test/unittest_reporting.py b/test/unittest_reporting.py index 8fda31d..3dd0d0a 100644 --- a/test/unittest_reporting.py +++ b/test/unittest_reporting.py @@ -10,7 +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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import os from os.path import join, dirname, abspath diff --git a/testutils.py b/testutils.py index a61fa7f..1658e10 100644 --- a/testutils.py +++ b/testutils.py @@ -12,17 +12,19 @@ # # 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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """functional/non regression tests for pylint""" +from __future__ import with_statement import collections import contextlib +import functools import sys import re from glob import glob from os import linesep -from os.path import abspath, dirname, join, basename, splitext +from os.path import abspath, basename, dirname, isdir, join, splitext from cStringIO import StringIO from logilab.common import testlib @@ -75,7 +77,8 @@ def get_tests_info(input_dir, msg_dir, prefix, suffix): if py_rest.isdigit() and SYS_VERS_STR >= py_rest: break else: - outfile = None + # This will provide an error message indicating the missing filename. + outfile = join(msg_dir, fbase + '.txt') result.append((infile, outfile)) return result @@ -146,6 +149,23 @@ class UnittestLinter(object): def add_stats(self, **kwargs): for name, value in kwargs.iteritems(): self.stats[name] = value + return self.stats + + +def set_config(**kwargs): + """Decorator for setting config values on a checker.""" + def _Wrapper(fun): + @functools.wraps(fun) + def _Forward(self): + for key, value in kwargs.iteritems(): + setattr(self.checker.config, key, value) + if isinstance(self, CheckerTestCase): + # reopen checker in case, it may be interested in configuration change + self.checker.open() + fun(self) + + return _Forward + return _Wrapper class CheckerTestCase(testlib.TestCase): @@ -159,7 +179,6 @@ class CheckerTestCase(testlib.TestCase): for key, value in self.CONFIG.iteritems(): setattr(self.checker.config, key, value) self.checker.open() - self.checker.stats = self.linter.stats @contextlib.contextmanager def assertNoMessages(self): @@ -236,6 +255,9 @@ class LintTestUsingModule(testlib.TestCase): for name, file in self.depends] self._test(tocheck) + def _check_result(self, got): + self.assertMultiLineEqual(self._get_expected(), got) + def _test(self, tocheck): if INFO_TEST_RGX.match(self.module): self.linter.enable('I') @@ -250,29 +272,44 @@ class LintTestUsingModule(testlib.TestCase): print ex ex.__str__ = exception_str raise - got = self.linter.reporter.finalize() - self.assertMultiLineEqual(self._get_expected(), got) + self._check_result(self.linter.reporter.finalize()) + def _has_output(self): + return not self.module.startswith('func_noerror_') def _get_expected(self): - if self.module.startswith('func_noerror_'): - expected = '' + if self._has_output() and self.output: + with open(self.output, 'U') as fobj: + return fobj.read().strip() + '\n' else: - output = open(self.output, 'U') - expected = output.read().strip() + '\n' - output.close() - return expected + return '' class LintTestUsingFile(LintTestUsingModule): _TEST_TYPE = 'file' def test_functionality(self): - tocheck = [join(self.INPUT_DIR, self.module + '.py')] + importable = join(self.INPUT_DIR, self.module) + # python also prefers packages over simple modules. + if not isdir(importable): + importable += '.py' + tocheck = [importable] if self.depends: tocheck += [join(self.INPUT_DIR, name) for name, _file in self.depends] self._test(tocheck) +class LintTestUpdate(LintTestUsingModule): + + _TEST_TYPE = 'update' + + def _check_result(self, got): + if self._has_output(): + if got != self._get_expected(): + if not self.output: + self.output = join(self.MSG_DIR, '%s.txt' % (self.module,)) + with open(self.output, 'w') as fobj: + fobj.write(got) + # Callback def cb_test_gen(base_class): @@ -299,8 +336,9 @@ def make_tests(input_dir, msg_dir, filter_rgx, callbacks): else: is_to_run = lambda x: 1 tests = [] - for module_file, messages_file in get_tests_info(input_dir, msg_dir, - 'func_', '.py'): + for module_file, messages_file in ( + get_tests_info(input_dir, msg_dir, 'func_', '') + ): if not is_to_run(module_file): continue base = module_file.replace('func_', '').replace('.py', '') @@ -6,5 +6,5 @@ envlist = py27, py33 [testenv] deps = logilab-common - astroid -commands = pytest -t {envsitepackagesdir}/pylint/test/
\ No newline at end of file + hg+https://bitbucket.org/logilab/astroid/ +commands = pytest -t {envsitepackagesdir}/pylint/test/ @@ -12,7 +12,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. +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """some various utilities and helper classes, most of them used in the main pylint class """ @@ -247,7 +247,7 @@ class MessagesHandlerMixIn(object): self._alternative_names[old_symbol] = msg self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid) - def disable(self, msgid, scope='package', line=None): + def disable(self, msgid, scope='package', line=None, ignore_unknown=False): """don't output message of the given id""" assert scope in ('package', 'module') # handle disable=all by disabling all categories @@ -272,8 +272,15 @@ class MessagesHandlerMixIn(object): if msgid.lower().startswith('rp'): self.disable_report(msgid) return - # msgid is a symbolic or numeric msgid. - msg = self.check_message_id(msgid) + + try: + # msgid is a symbolic or numeric msgid. + msg = self.check_message_id(msgid) + except UnknownMessage: + if ignore_unknown: + return + raise + if scope == 'module': assert line > 0 try: @@ -290,7 +297,7 @@ class MessagesHandlerMixIn(object): self.config.disable_msg = [mid for mid, val in msgs.iteritems() if not val] - def enable(self, msgid, scope='package', line=None): + def enable(self, msgid, scope='package', line=None, ignore_unknown=False): """reenable message of the given id""" assert scope in ('package', 'module') catid = category_id(msgid) @@ -309,8 +316,15 @@ class MessagesHandlerMixIn(object): if msgid.lower().startswith('rp'): self.enable_report(msgid) return - # msgid is a symbolic or numeric msgid. - msg = self.check_message_id(msgid) + + try: + # msgid is a symbolic or numeric msgid. + msg = self.check_message_id(msgid) + except UnknownMessage: + if ignore_unknown: + return + raise + if scope == 'module': assert line > 0 try: |