From 5ae9fd56321d2c81a440aa440fc505b79d4300cb Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 22 Sep 2013 23:27:55 +0300 Subject: Add basic checks for Python 3 format strings. --- checkers/strings.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++------- checkers/utils.py | 35 +++++++++++++++++++- 2 files changed, 118 insertions(+), 12 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 42563da..c4a6a39 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -41,8 +41,10 @@ MSGS = { conversion specifier."), 'E1302': ("Mixing named and unnamed conversion specifiers in format string", "mixed-format-string", - "Used when a format string contains both named (e.g. '%(foo)d') \ - and unnamed (e.g. '%d') conversion specifiers. This is also \ + "Used when a format string contains both named (e.g. '%(foo)d' + or '{foo}' for Python 3 format string) \ + and unnamed (e.g. '%d' or '{}' for Python 3 format string) \ + conversion specifiers. This is also \ used when a named conversion specifier contains * for the \ minimum field width and/or precision."), 'E1303': ("Expected mapping for format string, not %s", @@ -71,12 +73,42 @@ MSGS = { "too-few-format-args", "Used when a format string that uses unnamed conversion \ specifiers is given too many arguments"), + + 'E1307': ("Invalid Python 3 format string", + "bad-format-string", + "Used when a Python 3 format string is invalid"), + 'E1308': ("Missing keyword argument %r for format string", + "missing-format-argument-key", + "Used when a Python 3 format string that uses named specifiers \ + doesn't receive one or more required keywords."), + 'W1302': ("Unused format argument %r", + "unused-format-string-argument", + "Used when a Python 3 format string that uses named \ + conversion specifiers is used with an argument that \ + is not required by the format string."), + } OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, astroid.Lambda, astroid.Function, astroid.ListComp, astroid.SetComp, astroid.GenExpr) +def get_args(callfunc): + """ Get the arguments from the given `CallFunc` node. + Return a tuple, where the first element is the + number of positional arguments and the second element + is the keyword arguments in a dict + """ + positional = 0 + named = {} + + for arg in callfunc.args: + if isinstance(arg, astroid.Keyword): + named[arg.arg] = utils.safe_infer(arg.value) + else: + positional += 1 + return positional, named + class StringFormatChecker(BaseChecker): """Checks string formatting operations to ensure that the format string is valid and the arguments match the format string. @@ -182,16 +214,57 @@ class StringMethodsChecker(BaseChecker): func = utils.safe_infer(node.func) if (isinstance(func, astroid.BoundMethod) and isinstance(func.bound, astroid.Instance) - and func.bound.name in ('str', 'unicode', 'bytes') - and func.name in ('strip', 'lstrip', 'rstrip') - and node.args): - arg = utils.safe_infer(node.args[0]) - if not isinstance(arg, astroid.Const): - return - if len(arg.value) != len(set(arg.value)): - self.add_message('E1310', node=node, - args=(func.bound.name, func.name)) + and func.bound.name in ('str', 'unicode', 'bytes')): + + if func.name in ('strip', 'lstrip', 'rstrip') and node.args: + arg = utils.safe_infer(node.args[0]) + if not isinstance(arg, astroid.Const): + return + if len(arg.value) != len(set(arg.value)): + self.add_message('E1310', node=node, + args=(func.bound.name, func.name)) + elif func.name == 'format': + self._parse_format(node, func) + + def _parse_format(self, node, func): + try: + strnode = func.bound.infer().next() + except astroid.InferenceError: + return + + try: + positional, named = get_args(node) + except astroid.InferenceError: + return + + try: + required_keys, required_num_args = \ + utils.parse_format_method_string(strnode.value) + except utils.IncompleteFormatString: + self.add_message('bad-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('mixed-format-string', node=node) + elif required_keys: + for key in required_keys: + if key not in named: + self.add_message('missing-format-argument-key', + node=node, + args=(key, )) + for key in named: + if key not in required_keys: + self.add_message('unused-format-string-argument', + node=node, + args=(key, )) + else: + if positional > required_num_args: + self.add_message('too-many-format-args', node=node) + elif positional < required_num_args: + self.add_message('too-few-format-args', node=node) + class StringConstantChecker(BaseTokenChecker): """Check string literals""" diff --git a/checkers/utils.py b/checkers/utils.py index 68b5469..119a859 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -26,9 +26,13 @@ from astroid import scoped_nodes from logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ - COMP_NODE_TYPES = astroid.ListComp, astroid.SetComp, astroid.DictComp, astroid.GenExpr +if hasattr(string, 'Formatter'): + STRING_FORMATTER = string.Formatter() +else: + STRING_FORMATTER = None + class NoSuchArgumentError(Exception): pass @@ -346,6 +350,35 @@ def parse_format_string(format_string): i += 1 return keys, num_args +def parse_format_method_string(format_string): + """Parses a Python 3 format string, returning a tuple of (keys, num_args), + where keys is the set of mapping keys in the format string, and num_args + is the number of arguments required by the format string. + """ + keys = set() + num_args = 0 + + # TODO: should raise a custom exception? + if not STRING_FORMATTER: + return keys, num_args + + parseiterator = STRING_FORMATTER.parse(format_string) + try: + for result in parseiterator: + if all(item is None for item in result[1:]): + # not a replacement format + continue + name = result[1] + if name: + keys.add(name) + else: + num_args += 1 + except ValueError: + # probably the format string is invalid + # should we check the argument of the ValueError? + raise IncompleteFormatString(format_string) + return keys, num_args + def is_attr_protected(attrname): """return True if attribute name is protected (start with _ and some other details), False otherwise. -- cgit v1.2.1 From 550d280f18507283d90e4e50cd8d35d8a6d2deaa Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 22 Sep 2013 23:34:04 +0300 Subject: Update Changelog. --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index 3662602..e231aea 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,8 @@ ChangeLog for Pylint * Added a new warning, 'non-iterator-returned', for non-iterators returned by '__iter__' + * Add new checks for Python 3 format strings + 2013-08-06 -- 1.0.0 -- cgit v1.2.1 From 054f96efb204cc2ddbf287e52fd4a4bd734dc6bc Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 22 Sep 2013 23:36:27 +0300 Subject: Bug fix. --- checkers/strings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/strings.py b/checkers/strings.py index c4a6a39..f98e70d 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -41,7 +41,7 @@ MSGS = { conversion specifier."), 'E1302': ("Mixing named and unnamed conversion specifiers in format string", "mixed-format-string", - "Used when a format string contains both named (e.g. '%(foo)d' + "Used when a format string contains both named (e.g. '%(foo)d' \ or '{foo}' for Python 3 format string) \ and unnamed (e.g. '%d' or '{}' for Python 3 format string) \ conversion specifiers. This is also \ -- cgit v1.2.1 From a4d15ce1c88d8e6b8e48ccb3bf08d5abbb52723a Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 23 Sep 2013 00:28:34 +0300 Subject: Add tests for Python 3 string format checker. --- test/input/func_string_format_py26.py | 16 ++++++++++++++++ test/messages/func_string_format_py26.txt | 13 +++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 test/input/func_string_format_py26.py create mode 100644 test/messages/func_string_format_py26.txt diff --git a/test/input/func_string_format_py26.py b/test/input/func_string_format_py26.py new file mode 100644 index 0000000..33c5798 --- /dev/null +++ b/test/input/func_string_format_py26.py @@ -0,0 +1,16 @@ +"""test for Python 3 string formatting error +""" + +__revision__ = 1 + +def pprint(): + """Test string format """ + print "{} {".format() + print "{} }".format() + print "{} {a}".format(1, 2) + print "{} {}".format(1) + print "{} {}".format(1, 2, 3) + print "{a} {b}".format(a=1, c=2) + print "{a} {b} {c}".format() + print "{} {}".format(a=1, b=2) + print "{a} {b}".format(1, 2) diff --git a/test/messages/func_string_format_py26.txt b/test/messages/func_string_format_py26.txt new file mode 100644 index 0000000..d60ee64 --- /dev/null +++ b/test/messages/func_string_format_py26.txt @@ -0,0 +1,13 @@ +E: 8:pprint: Invalid Python 3 format string +E: 9:pprint: Invalid Python 3 format string +E: 10:pprint: Mixing named and unnamed conversion specifiers in format string +E: 11:pprint: Not enough arguments for format string +E: 12:pprint: Too many arguments for format string +E: 13:pprint: Missing keyword argument 'b' for format string +E: 14:pprint: Missing keyword argument 'a' for format string +E: 14:pprint: Missing keyword argument 'b' for format string +E: 14:pprint: Missing keyword argument 'c' for format string +E: 15:pprint: Not enough arguments for format string +E: 16:pprint: Missing keyword argument 'a' for format string +E: 16:pprint: Missing keyword argument 'b' for format string +W: 13:pprint: Unused format argument 'c' \ No newline at end of file -- cgit v1.2.1 From 09832b4a8bdebb30d7b3c29466b7fd6dd4212a6a Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 25 Sep 2013 22:44:25 +0300 Subject: Add new string format checks. --- checkers/strings.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/checkers/strings.py b/checkers/strings.py index f98e70d..13112cb 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -29,6 +29,7 @@ from pylint.checkers import utils from pylint.checkers.utils import check_messages _PY3K = sys.version_info >= (3, 0) +_PY26 = sys.version_info < (2, 7) MSGS = { 'E1300': ("Unsupported format character %r (%#02x) at index %d", @@ -87,6 +88,20 @@ MSGS = { conversion specifiers is used with an argument that \ is not required by the format string."), + 'W1303': ("Format string contains both automatic field numbering " + "and manual field specifiers", + "format-combined-specifiers", + "Usen when a format string contains both automatic \ + field numbering (e.g. '{}') and manual field \ + specification (e.g. '{0}')"), + 'W1304': ("Can't use automatic field numbering for this version", + "no-automatic-field-numbering", + "Usen for Python versions lower than 2.7 when a " + "format string is used with automatic field numbering " + "(e.g. '{}'). Only manual field numbering (e.g. '{0}') " + "or named fields (e.g. '{a}') can work.", + {'maxversion': (2, 6)}), + } OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, @@ -244,6 +259,17 @@ class StringMethodsChecker(BaseChecker): self.add_message('bad-format-string', node=node) return + manual_keys = set([key for key in required_keys + if key.isdigit()]) + if required_keys - manual_keys: + self.add_message('format-combined-specifiers', + node=node) + return + + if _PY26 and required_num_args: + self.add_message('no-automatic-field-numbering', + node=node) + if required_keys and required_num_args: # The format string uses both named and unnamed format # specifiers. -- cgit v1.2.1 From e191027464f9800fca4e525773a68daf536d372d Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 25 Sep 2013 23:02:24 +0300 Subject: Proper handling of manual field specifiers. --- checkers/strings.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 13112cb..dd1de2c 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -261,7 +261,7 @@ class StringMethodsChecker(BaseChecker): manual_keys = set([key for key in required_keys if key.isdigit()]) - if required_keys - manual_keys: + if manual_keys and required_keys - manual_keys: self.add_message('format-combined-specifiers', node=node) return @@ -276,7 +276,7 @@ class StringMethodsChecker(BaseChecker): self.add_message('mixed-format-string', node=node) elif required_keys: for key in required_keys: - if key not in named: + if key not in named and not key.isdigit(): self.add_message('missing-format-argument-key', node=node, args=(key, )) @@ -289,8 +289,10 @@ class StringMethodsChecker(BaseChecker): if positional > required_num_args: self.add_message('too-many-format-args', node=node) elif positional < required_num_args: - self.add_message('too-few-format-args', node=node) - + self.add_message('too-few-format-args', node=node) + + if manual_keys and positional < len(manual_keys): + self.add_message('too-few-format-args', node=node) class StringConstantChecker(BaseTokenChecker): """Check string literals""" -- cgit v1.2.1 From 14a9934b1d97208cfe9aa404755f0bf5d820b8b6 Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 3 Oct 2013 17:54:11 +0300 Subject: Various changes to format checker. --- checkers/strings.py | 56 ++++++++++++++++++++++++++++++++++++++++++++--------- checkers/utils.py | 33 ------------------------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index dd1de2c..28ad10a 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -20,6 +20,7 @@ import sys import tokenize +import string import astroid @@ -29,7 +30,8 @@ from pylint.checkers import utils from pylint.checkers.utils import check_messages _PY3K = sys.version_info >= (3, 0) -_PY26 = sys.version_info < (2, 7) +_PY26 = sys.version_info == (2, 7) +_GT_PY25 = sys.version_info > (2, 5) MSGS = { 'E1300': ("Unsupported format character %r (%#02x) at index %d", @@ -108,6 +110,45 @@ OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, astroid.Lambda, astroid.Function, astroid.ListComp, astroid.SetComp, astroid.GenExpr) +if _PY3K: + import _string + + def split_format_field_names(format_string): + return _string.formatter_field_name_split(format_string) +else: + def split_format_field_names(format_string): + return format_string._formatter_field_name_split() + +def parse_format_method_string(format_string): + """ + Parses a Python 3 format string, returning a tuple of + (keys, num_args), + where keys is the set of mapping keys in the format string, num_args + is the number of arguments required by the format string and accessors + is a dictionary of attribute accessors like '{a.field}' or element + accessor, like '{0[2]}'. + """ + keys = [] + num_args = 0 + formatter = string.Formatter() + parseiterator = formatter.parse(format_string) + try: + for result in parseiterator: + if all(item is None for item in result[1:]): + # not a replacement format + continue + name = result[1] + if name: + keyname, fielditerator = split_format_field_names(name) + keys.append((keyname, list(fielditerator))) + else: + num_args += 1 + except ValueError: + # probably the format string is invalid + # should we check the argument of the ValueError? + raise utils.IncompleteFormatString(format_string) + return keys, num_args + def get_args(callfunc): """ Get the arguments from the given `CallFunc` node. Return a tuple, where the first element is the @@ -239,7 +280,8 @@ class StringMethodsChecker(BaseChecker): self.add_message('E1310', node=node, args=(func.bound.name, func.name)) elif func.name == 'format': - self._parse_format(node, func) + if _GT_PY25: + self._parse_format(node, func) def _parse_format(self, node, func): try: @@ -254,14 +296,14 @@ class StringMethodsChecker(BaseChecker): try: required_keys, required_num_args = \ - utils.parse_format_method_string(strnode.value) + parse_format_method_string(strnode.value) except utils.IncompleteFormatString: self.add_message('bad-format-string', node=node) return manual_keys = set([key for key in required_keys if key.isdigit()]) - if manual_keys and required_keys - manual_keys: + if manual_keys and required_num_args: self.add_message('format-combined-specifiers', node=node) return @@ -270,11 +312,7 @@ class StringMethodsChecker(BaseChecker): self.add_message('no-automatic-field-numbering', node=node) - if required_keys and required_num_args: - # The format string uses both named and unnamed format - # specifiers. - self.add_message('mixed-format-string', node=node) - elif required_keys: + if required_keys: for key in required_keys: if key not in named and not key.isdigit(): self.add_message('missing-format-argument-key', diff --git a/checkers/utils.py b/checkers/utils.py index 119a859..4a39119 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -28,11 +28,6 @@ from logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ COMP_NODE_TYPES = astroid.ListComp, astroid.SetComp, astroid.DictComp, astroid.GenExpr -if hasattr(string, 'Formatter'): - STRING_FORMATTER = string.Formatter() -else: - STRING_FORMATTER = None - class NoSuchArgumentError(Exception): pass @@ -350,34 +345,6 @@ def parse_format_string(format_string): i += 1 return keys, num_args -def parse_format_method_string(format_string): - """Parses a Python 3 format string, returning a tuple of (keys, num_args), - where keys is the set of mapping keys in the format string, and num_args - is the number of arguments required by the format string. - """ - keys = set() - num_args = 0 - - # TODO: should raise a custom exception? - if not STRING_FORMATTER: - return keys, num_args - - parseiterator = STRING_FORMATTER.parse(format_string) - try: - for result in parseiterator: - if all(item is None for item in result[1:]): - # not a replacement format - continue - name = result[1] - if name: - keys.add(name) - else: - num_args += 1 - except ValueError: - # probably the format string is invalid - # should we check the argument of the ValueError? - raise IncompleteFormatString(format_string) - return keys, num_args def is_attr_protected(attrname): """return True if attribute name is protected (start with _ and some other -- cgit v1.2.1 From ec431fc5f917f6f36a4a17ad39bd1aa88addd0f3 Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 19 Dec 2013 01:13:02 +0200 Subject: Partial merge with default. --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index e231aea..6db5344 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,8 @@ ChangeLog for Pylint * Added a new warning, 'non-iterator-returned', for non-iterators returned by '__iter__' + * Add new warning for unpacking non-sequences in assignments + * Add new checks for Python 3 format strings -- cgit v1.2.1 From e7991c5ba0903137308cfa91092e9296e2b7d7ab Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 19 Dec 2013 01:14:40 +0200 Subject: Hopefully merge fix. --- ChangeLog | 2 -- 1 file changed, 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6db5344..34a38d6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,8 +12,6 @@ ChangeLog for Pylint returned by '__iter__' * Add new warning for unpacking non-sequences in assignments - - * Add new checks for Python 3 format strings 2013-08-06 -- 1.0.0 -- cgit v1.2.1 From 507d900bf09f718907cfafb0c7703a777b7f79c2 Mon Sep 17 00:00:00 2001 From: Manuel V?zquez Acosta Date: Mon, 6 Jan 2014 15:31:19 -0500 Subject: Injects the current sys.path into `lint.py` subprocess env This allows installing pylint in a buildout setup and *see* the eggs in the buildout. Since buildout modifies the installed scripts (like epyling) by prepending and assignment to ``sys.path[0:0]`` all the eggs, we need to make sure those path are the ones seen by pylint. --- epylint.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/epylint.py b/epylint.py index 24baa61..13449b2 100755 --- a/epylint.py +++ b/epylint.py @@ -83,7 +83,18 @@ def lint(filename, options=None): options = options or ['--disable=C,R,I'] cmd = [sys.executable, lint_path] + options + ['--msg-template', '{path}:{line}: [{symbol}, {obj}] {msg}', '-r', 'n', child_path] - process = Popen(cmd, stdout=PIPE, cwd=parent_path, universal_newlines=True) + # Extracts the environment PYTHONPATH and appends the current sys.path to + # those. + env = dict(os.environ) + pythonpath = env.get('PYTHONPATH', '') + currentpaths = os.pathsep.join(sys.path) + if pythonpath: + pythonpath += os.pathsep + currentpaths + else: + pythonpath = currentpaths + env['PYTHONPATH'] = pythonpath + process = Popen(cmd, stdout=PIPE, cwd=parent_path, env=env, + 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 .. @@ -157,9 +168,20 @@ def py_run(command_options='', return_std=False, stdout=None, stderr=None, stderr = PIPE else: stderr = sys.stderr + # Extracts the environment PYTHONPATH and appends the current sys.path to + # those. + env = dict(os.environ) + pythonpath = env.get('PYTHONPATH', '') + currentpaths = os.pathsep.join(sys.path) + if pythonpath: + pythonpath += os.pathsep + currentpaths + else: + pythonpath = currentpaths + env['PYTHONPATH'] = pythonpath + # Call pylint in a subprocess p = Popen(command_line, shell=True, stdout=stdout, stderr=stderr, - universal_newlines=True) + env=env, universal_newlines=True) p.wait() # Return standart output and error if return_std: @@ -179,4 +201,3 @@ def Run(): if __name__ == '__main__': Run() - -- cgit v1.2.1 From 6e843239561b38b2bcbe4f5390a08d020cf10b13 Mon Sep 17 00:00:00 2001 From: Manuel V?zquez Acosta Date: Mon, 6 Jan 2014 16:07:33 -0500 Subject: Refactors the PYTHONPATH detector into a function --- epylint.py | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/epylint.py b/epylint.py index 13449b2..cd8dd26 100755 --- a/epylint.py +++ b/epylint.py @@ -50,6 +50,18 @@ import sys, os, re import os.path as osp from subprocess import Popen, PIPE +def _get_env(): + '''Extracts the environment PYTHONPATH and appends the current sys.path to + those.''' + env = dict(os.environ) + pythonpath = env.get('PYTHONPATH', '') + currentpaths = os.pathsep.join(sys.path) + if pythonpath: + pythonpath += os.pathsep + currentpaths + else: + pythonpath = currentpaths + env['PYTHONPATH'] = pythonpath + return env def lint(filename, options=None): """Pylint the given file. @@ -83,17 +95,7 @@ def lint(filename, options=None): options = options or ['--disable=C,R,I'] cmd = [sys.executable, lint_path] + options + ['--msg-template', '{path}:{line}: [{symbol}, {obj}] {msg}', '-r', 'n', child_path] - # Extracts the environment PYTHONPATH and appends the current sys.path to - # those. - env = dict(os.environ) - pythonpath = env.get('PYTHONPATH', '') - currentpaths = os.pathsep.join(sys.path) - if pythonpath: - pythonpath += os.pathsep + currentpaths - else: - pythonpath = currentpaths - env['PYTHONPATH'] = pythonpath - process = Popen(cmd, stdout=PIPE, cwd=parent_path, env=env, + process = Popen(cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), universal_newlines=True) # The parseable line format is '%(path)s:%(line)s: [%(sigle)s%(obj)s] %(msg)s' @@ -168,20 +170,9 @@ def py_run(command_options='', return_std=False, stdout=None, stderr=None, stderr = PIPE else: stderr = sys.stderr - # Extracts the environment PYTHONPATH and appends the current sys.path to - # those. - env = dict(os.environ) - pythonpath = env.get('PYTHONPATH', '') - currentpaths = os.pathsep.join(sys.path) - if pythonpath: - pythonpath += os.pathsep + currentpaths - else: - pythonpath = currentpaths - env['PYTHONPATH'] = pythonpath - # Call pylint in a subprocess p = Popen(command_line, shell=True, stdout=stdout, stderr=stderr, - env=env, universal_newlines=True) + env=_get_env(), universal_newlines=True) p.wait() # Return standart output and error if return_std: -- cgit v1.2.1 From 11d3c4903929d001c3c3d89445fd814a1d8a33c9 Mon Sep 17 00:00:00 2001 From: Holger Peters Date: Mon, 7 Apr 2014 15:53:48 +0200 Subject: Add 'ignored-modules' option to type checker to avoid false-positives w/ numpy --- checkers/typecheck.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 58ac68d..a4f60ba 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -94,7 +94,14 @@ class TypeChecker(BaseChecker): class should be ignored. A mixin class is detected if its name ends with \ "mixin" (case insensitive).'} ), - + ('ignored-modules', + {'default': ('numpy',), + 'type': 'csv', + 'metavar': '', + 'help': 'List of module names for which member attributes \ +should not be checked (useful for modules/projects where namespaces are \ +manipulated during runtime'}, + ), ('ignored-classes', {'default' : ('SQLObject',), 'type' : 'csv', @@ -192,7 +199,8 @@ accessed. Python regular expressions are accepted.'} if isinstance(owner, Instance) and owner.has_dynamic_getattr(): continue # explicit skipping of optparse'Values class - if owner.name == 'Values' and owner.root().name == 'optparse': + if (owner.name == 'Values' and owner.root().name == 'optparse' + or owner.name in self.config.ignored_modules): continue missingattr.add((owner, name)) continue -- cgit v1.2.1 From 98fb9dcd69706529d8c21dfa465c28a38e46ef5f Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 12 Apr 2014 08:52:46 +0300 Subject: Use the infered name when accessing __all__ elements. Closes 188. --- checkers/variables.py | 2 +- test/input/func_all.py | 3 ++- test/messages/func_all.txt | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/checkers/variables.py b/checkers/variables.py index 7c489e8..469ecca 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -213,7 +213,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' if not isinstance(elt_name, astroid.Const) or not isinstance(elt_name.value, basestring): self.add_message('E0604', args=elt.as_string(), node=elt) continue - elt_name = elt.value + elt_name = elt_name.value # If elt is in not_consumed, remove it from not_consumed if elt_name in not_consumed: del not_consumed[elt_name] diff --git a/test/input/func_all.py b/test/input/func_all.py index e9f1fc8..0fd75dc 100644 --- a/test/input/func_all.py +++ b/test/input/func_all.py @@ -11,6 +11,7 @@ __revision__ = 0 from os import path +from collections import deque __all__ = [ 'Dummy', @@ -18,7 +19,7 @@ __all__ = [ 'path', 'func', 'inner', - 'InnerKlass'] + 'InnerKlass', deque.__name__] class Dummy(object): diff --git a/test/messages/func_all.txt b/test/messages/func_all.txt index ba0e855..fb314a7 100644 --- a/test/messages/func_all.txt +++ b/test/messages/func_all.txt @@ -1,4 +1,4 @@ -E: 17: Undefined variable name 'NonExistant' in __all__ -E: 19: Undefined variable name 'func' in __all__ -E: 20: Undefined variable name 'inner' in __all__ -E: 21: Undefined variable name 'InnerKlass' in __all__ +E: 18: Undefined variable name 'NonExistant' in __all__ +E: 20: Undefined variable name 'func' in __all__ +E: 21: Undefined variable name 'inner' in __all__ +E: 22: Undefined variable name 'InnerKlass' in __all__ -- cgit v1.2.1 From 2453052e298cfef361af0ddd17c4dd05f85ad48d Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 12 Apr 2014 15:52:21 +0300 Subject: Infer slots elements before attempting to validate them. --- checkers/classes.py | 29 ++++++++++++++++++----------- test/input/func_bad_slots.py | 7 ++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/checkers/classes.py b/checkers/classes.py index 85a8507..f466401 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -373,18 +373,25 @@ a metaclass class method.'} 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) + try: + self._check_slots_elt(elt) + except astroid.InferenceError: continue - if not elt.value: - self.add_message('invalid-slots-object', - args=elt.as_string(), - node=elt) + + def _check_slots_elt(self, elt): + for infered in elt.infer(): + if infered is YES: + continue + if (not isinstance(infered, astroid.Const) or + not isinstance(infered.value, str)): + self.add_message('invalid-slots-object', + args=infered.as_string(), + node=elt) + continue + if not infered.value: + self.add_message('invalid-slots-object', + args=infered.as_string(), + node=elt) def _check_iter(self, node): try: diff --git a/test/input/func_bad_slots.py b/test/input/func_bad_slots.py index 16f7a79..bfb83d7 100644 --- a/test/input/func_bad_slots.py +++ b/test/input/func_bad_slots.py @@ -54,4 +54,9 @@ class FifthBad(object): class PotentiallyGood(object): __slots__ = func() - \ No newline at end of file + +class PotentiallySecondGood(object): + __slots__ = ('a', deque.__name__) + +class PotentiallyThirdGood(object): + __slots__ = deque.__name__ -- cgit v1.2.1 From 20d3d09bbacfb54d96174b22a84cda820c2dfb33 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 08:51:12 +0200 Subject: Add back the -i and -s options for backwards compat, but have them be ignored. Closes #180 --- ChangeLog | 3 +++ lint.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index f9082ac..7e57cd6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,9 @@ ChangeLog for Pylint -- + * Add -i / --include-ids and -s / --symbols back as completely ignored + options. Fixes BitBucket issue #180. + * 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. diff --git a/lint.py b/lint.py index 0e1afa0..e69bc49 100644 --- a/lint.py +++ b/lint.py @@ -155,6 +155,13 @@ MSGS = { } +def _deprecated_option(shortname, opt_type): + def _warn_deprecated(option, optname, *args): + sys.stderr.write('Warning: option %s is deprecated and ignored.\n' % (optname,)) + return {'short': shortname, 'help': 'DEPRECATED', 'hide': True, + 'type': opt_type, 'action': 'callback', 'callback': _warn_deprecated} + + class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, BaseTokenChecker): """lint Python modules using external checkers. @@ -272,7 +279,10 @@ warning, statement which respectively contain the number of errors / warnings\ 'This is a python new-style format string ' 'used to format the message information. ' 'See doc for all details') - }), # msg-template + }), + + ('include-ids', _deprecated_option('i', 'yn')), + ('symbols', _deprecated_option('s', 'yn')), ) option_groups = ( -- cgit v1.2.1 From 0393e2a4b432a916847b4a80fbb2c7522887a0b8 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 00:28:12 +0200 Subject: Detect logging calls on Logger instances in almost all cases. --- ChangeLog | 3 + checkers/logging.py | 81 ++++++++++++---------- test/input/func_logging_not_lazy_with_logger.py | 15 ++++ .../messages/func_logging_not_lazy_with_logger.txt | 4 ++ 4 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 test/input/func_logging_not_lazy_with_logger.py create mode 100644 test/messages/func_logging_not_lazy_with_logger.txt diff --git a/ChangeLog b/ChangeLog index 7e57cd6..6085467 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,9 @@ ChangeLog for Pylint * Add -i / --include-ids and -s / --symbols back as completely ignored options. Fixes BitBucket issue #180. + * Extend the number of cases in which logging calls are detected. Fixes + bitbucket issue #182. + * 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. diff --git a/checkers/logging.py b/checkers/logging.py index cc8967d..dbd1e7b 100644 --- a/checkers/logging.py +++ b/checkers/logging.py @@ -103,48 +103,55 @@ class LoggingChecker(checkers.BaseChecker): @check_messages(*(MSGS.keys())) def visit_callfunc(self, node): - """Checks calls to (simple forms of) logging methods.""" - if (not isinstance(node.func, astroid.Getattr) - or not isinstance(node.func.expr, astroid.Name)): - return - try: - logger_class = [inferred for inferred in node.func.expr.infer() if ( - isinstance(inferred, astroid.Instance) - and any(ancestor for ancestor in inferred._proxied.ancestors() if ( - ancestor.name == 'Logger' - and ancestor.parent.name == 'logging')))] - except astroid.exceptions.InferenceError: - return - 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) + """Checks calls to logging methods.""" + def is_logging_name(): + return (isinstance(node.func, astroid.Getattr) and + isinstance(node.func.expr, astroid.Name) and + node.func.expr.name in self._logging_names) - def _check_convenience_methods(self, node): - """Checks calls to logging convenience methods (like logging.warn).""" - if node.func.attrname not in CHECKED_CONVENIENCE_FUNCTIONS: - return - if node.starargs or node.kwargs or not node.args: - # Either no args, star args, or double-star args. Beyond the - # scope of this checker. - return - if isinstance(node.args[0], astroid.BinOp) and node.args[0].op == '%': - self.add_message('W1201', node=node) - elif isinstance(node.args[0], astroid.Const): - self._check_format_string(node, 0) + def is_logger_class(): + try: + for inferred in node.func.infer(): + if isinstance(inferred, astroid.BoundMethod): + parent = inferred._proxied.parent + if (isinstance(parent, astroid.Class) and + (parent.qname() == 'logging.Logger' or + any(ancestor.qname() == 'logging.Logger' + for ancestor in parent.ancestors()))): + return True, inferred._proxied.name + except astroid.exceptions.InferenceError: + pass + return False, None + + if is_logging_name(): + name = node.func.attrname + else: + result, name = is_logger_class() + if not result: + return + self._check_log_method(node, name) - def _check_log_methods(self, node): + def _check_log_method(self, node, name): """Checks calls to logging.log(level, format, *format_args).""" - if node.func.attrname != 'log': - return - if node.starargs or node.kwargs or len(node.args) < 2: - # Either a malformed call, star args, or double-star args. Beyond - # the scope of this checker. + if name == 'log': + if node.starargs or node.kwargs or len(node.args) < 2: + # Either a malformed call, star args, or double-star args. Beyond + # the scope of this checker. + return + format_pos = 1 + elif name in CHECKED_CONVENIENCE_FUNCTIONS: + if node.starargs or node.kwargs or not node.args: + # Either no args, star args, or double-star args. Beyond the + # scope of this checker. + return + format_pos = 0 + else: return - if isinstance(node.args[1], astroid.BinOp) and node.args[1].op == '%': + + if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format_pos].op == '%': self.add_message('W1201', node=node) - elif isinstance(node.args[1], astroid.Const): - self._check_format_string(node, 1) + elif isinstance(node.args[format_pos], astroid.Const): + self._check_format_string(node, format_pos) def _check_format_string(self, node, format_arg): """Checks that format string tokens match the supplied arguments. diff --git a/test/input/func_logging_not_lazy_with_logger.py b/test/input/func_logging_not_lazy_with_logger.py new file mode 100644 index 0000000..c625a07 --- /dev/null +++ b/test/input/func_logging_not_lazy_with_logger.py @@ -0,0 +1,15 @@ +"""Logging warnings using a logger class.""" + +__revision__ = '' + +import logging + + +LOG = logging.getLogger("domain") +LOG.debug("%s" % "junk") +LOG.log(logging.DEBUG, "%s" % "junk") +LOG2 = LOG.debug +LOG2("%s" % "junk") + +logging.getLogger("domain").debug("%s" % "junk") + diff --git a/test/messages/func_logging_not_lazy_with_logger.txt b/test/messages/func_logging_not_lazy_with_logger.txt new file mode 100644 index 0000000..8b483a7 --- /dev/null +++ b/test/messages/func_logging_not_lazy_with_logger.txt @@ -0,0 +1,4 @@ +W: 9: Specify string format arguments as logging function parameters +W: 10: Specify string format arguments as logging function parameters +W: 12: Specify string format arguments as logging function parameters +W: 14: Specify string format arguments as logging function parameters -- cgit v1.2.1 From 21e07bee2344ce45e4cee7aa9fccf2725c4b4d95 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 19:49:11 +0200 Subject: Emit only symbolic names from the classes checker. --- checkers/classes.py | 59 ++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/checkers/classes.py b/checkers/classes.py index f466401..fc2e2d6 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -248,10 +248,10 @@ a metaclass class method.'} try: node.local_attr('__init__') except astroid.NotFoundError: - self.add_message('W0232', args=node, node=node) + self.add_message('no-init', args=node, node=node) self._check_slots(node) - @check_messages('E0203', 'W0201') + @check_messages('access-member-before-definition', 'attribute-defined-outside-init') def leave_class(self, cnode): """close a class node: check that instance attributes are defined in __init__ and check @@ -262,7 +262,7 @@ a metaclass class method.'} if cnode.type != 'metaclass': self._check_accessed_members(cnode, accessed) # checks attributes are defined in an allowed method such as __init__ - if 'W0201' not in self.active_msgs: + if not self.linter.is_message_enabled('attribute-defined-outside-init'): return defining_methods = self.config.defining_attr_methods for attr, nodes in cnode.instance_attrs.iteritems(): @@ -291,7 +291,7 @@ a metaclass class method.'} try: cnode.local_attr(attr) except astroid.NotFoundError: - self.add_message('W0201', args=attr, node=node) + self.add_message('attribute-defined-outside-init', args=attr, node=node) def visit_function(self, node): """check method arguments, overriding""" @@ -333,7 +333,7 @@ a metaclass class method.'} try: overridden = klass.instance_attr(node.name)[0] # XXX args = (overridden.root().name, overridden.fromlineno) - self.add_message('E0202', args=args, node=node) + self.add_message('method-hidden', args=args, node=node) except astroid.NotFoundError: pass @@ -430,7 +430,7 @@ a metaclass class method.'} if node.is_method(): if node.args.args is not None: self._first_attrs.pop() - if 'R0201' not in self.active_msgs: + if not self.linter.is_message_enabled('no-self-use'): return class_node = node.parent.frame() if (self._meth_could_be_func and node.type == 'method' @@ -438,7 +438,7 @@ a metaclass class method.'} and not (node.is_abstract() or overrides_a_method(class_node, node.name)) and class_node.type != 'interface'): - self.add_message('R0201', node=node) + self.add_message('no-self-use', node=node) def visit_getattr(self, node): """check if the getattr is an access to a class member @@ -451,15 +451,13 @@ a metaclass class method.'} if self.is_first_attr(node): self._accessed[-1].setdefault(attrname, []).append(node) return - if 'W0212' not in self.active_msgs: + if not self.linter.is_message_enabled('protected-access'): return self._check_protected_attribute_access(node) + @check_messages('protected-access') def visit_assign(self, assign_node): - if 'W0212' not in self.active_msgs: - return - node = assign_node.targets[0] if not isinstance(node, AssAttr): return @@ -492,7 +490,7 @@ a metaclass class method.'} # We are not in a class, no remaining valid case if klass is None: - self.add_message('W0212', node=node, args=attrname) + self.add_message('protected-access', node=node, args=attrname) return # If the expression begins with a call to super, that's ok. @@ -504,7 +502,7 @@ a metaclass class method.'} # We are in a class, one remaining valid cases, Klass._attr inside # Klass if not (callee == klass.name or callee in klass.basenames): - self.add_message('W0212', node=node, args=attrname) + self.add_message('protected-access', node=node, args=attrname) def visit_name(self, node): """check if the name handle an access to a class member @@ -549,7 +547,7 @@ a metaclass class method.'} for _node in nodes: if _node.frame() is frame and _node.fromlineno < lno \ and not are_exclusive(_node.statement(), defstmt, ('AttributeError', 'Exception', 'BaseException')): - self.add_message('E0203', node=_node, + self.add_message('access-member-before-definition', node=_node, args=(attr, lno)) def _check_first_arg_for_type(self, node, metaclass=0): @@ -573,34 +571,34 @@ a metaclass class method.'} if (first_arg == 'self' or first_arg in self.config.valid_classmethod_first_arg or first_arg in self.config.valid_metaclass_classmethod_first_arg): - self.add_message('W0211', args=first, node=node) + self.add_message('bad-staticmethod-argument', args=first, node=node) return self._first_attrs[-1] = None # class / regular method with no args elif not node.args.args: - self.add_message('E0211', node=node) + self.add_message('no-method-argument', node=node) # metaclass elif metaclass: # metaclass __new__ or classmethod if node.type == 'classmethod': self._check_first_arg_config(first, self.config.valid_metaclass_classmethod_first_arg, node, - 'C0204', node.name) + 'bad-mcs-classmethod-argument', node.name) # metaclass regular method else: self._check_first_arg_config(first, - self.config.valid_classmethod_first_arg, node, 'C0203', + self.config.valid_classmethod_first_arg, node, 'bad-mcs-method-argument', node.name) # regular class else: # class method if node.type == 'classmethod': self._check_first_arg_config(first, - self.config.valid_classmethod_first_arg, node, 'C0202', + self.config.valid_classmethod_first_arg, node, 'bad-classmethod-argument', node.name) # regular method without self as argument elif first != 'self': - self.add_message('E0213', node=node) + self.add_message('no-self-argument', node=node) def _check_first_arg_config(self, first, config, node, message, method_name): @@ -632,7 +630,7 @@ a metaclass class method.'} # 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, + self.add_message('abstract-method', node=node, args=(method.name, owner.name)) def _check_interfaces(self, node): @@ -644,7 +642,7 @@ a metaclass class method.'} """filter interface objects, it should be classes""" if not isinstance(obj, astroid.Class): e0221_hack[0] = True - self.add_message('E0221', node=node, + self.add_message('interface-is-not-class', node=node, args=(obj.as_string(),)) return False return True @@ -661,7 +659,7 @@ a metaclass class method.'} try: method = node_method(node, name) except astroid.NotFoundError: - self.add_message('E0222', args=(name, iface.name), + self.add_message('missing-interface-method', args=(name, iface.name), node=node) continue # ignore inherited methods @@ -680,14 +678,15 @@ a metaclass class method.'} # Use as_string() for the message # FIXME: in case of multiple interfaces, find which one could not # be resolved - self.add_message('F0220', node=implements, + self.add_message('unresolved-interface', node=implements, args=(node.name, assignment.value.as_string())) def _check_init(self, node): """check that the __init__ method call super or ancestors'__init__ method """ - if not set(('W0231', 'W0233')) & self.active_msgs: + if (not self.linter.is_message_enabled('super-init-not-called') and + not self.linter.is_message_enabled('non-parent-init-called')): return klass_node = node.parent.frame() to_call = _ancestors_to_call(klass_node) @@ -710,13 +709,13 @@ a metaclass class method.'} del not_called_yet[klass] except KeyError: if klass not in to_call: - self.add_message('W0233', node=expr, args=klass.name) + self.add_message('non-parent-init-called', node=expr, args=klass.name) except astroid.InferenceError: continue for klass, method in not_called_yet.iteritems(): if klass.name == 'object' or method.parent.name == 'object': continue - self.add_message('W0231', args=klass.name, node=node) + self.add_message('super-init-not-called', args=klass.name, node=node) def _check_signature(self, method1, refmethod, class_type): """check that the signature of the two given methods match @@ -725,7 +724,7 @@ a metaclass class method.'} """ if not (isinstance(method1, astroid.Function) and isinstance(refmethod, astroid.Function)): - self.add_message('F0202', args=(method1, refmethod), node=method1) + self.add_message('method-check-failed', args=(method1, refmethod), node=method1) return # don't care about functions with unknown argument (builtins) if method1.args.args is None or refmethod.args.args is None: @@ -736,9 +735,9 @@ a metaclass class method.'} if is_attr_private(method1.name): return if len(method1.args.args) != len(refmethod.args.args): - self.add_message('W0221', args=class_type, node=method1) + self.add_message('arguments-differ', args=class_type, node=method1) elif len(method1.args.defaults) < len(refmethod.args.defaults): - self.add_message('W0222', args=class_type, node=method1) + self.add_message('signature-differs', args=class_type, node=method1) def is_first_attr(self, node): """Check that attribute lookup name use first attribute variable name -- cgit v1.2.1 From e7f0d2854776affd89c745412848ac36bcb2396c Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 19:53:51 +0200 Subject: Only emit symbolic warnings from the imports checker. --- checkers/imports.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/checkers/imports.py b/checkers/imports.py index fd897a8..fa47edc 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -217,9 +217,9 @@ given file (report RP0402 must not be disabled)'} def close(self): """called before visiting project (i.e set of modules)""" # don't try to compute cycles if the associated message is disabled - if self.linter.is_message_enabled('R0401'): + if self.linter.is_message_enabled('cyclic-import'): for cycle in get_cycles(self.import_graph): - self.add_message('R0401', args=' -> '.join(cycle)) + self.add_message('cyclic-import', args=' -> '.join(cycle)) def visit_import(self, node): """triggered when an import statement is seen""" @@ -246,11 +246,11 @@ given file (report RP0402 must not be disabled)'} # consecutive future statements are possible if not (isinstance(prev, astroid.From) and prev.modname == '__future__'): - self.add_message('W0410', node=node) + self.add_message('misplaced-future', node=node) return for name, _ in node.names: if name == '*': - self.add_message('W0401', args=basename, node=node) + self.add_message('wildcard-import', args=basename, node=node) modnode = node.root() importedmodnode = self.get_imported_module(modnode, node, basename) if importedmodnode is None: @@ -270,14 +270,14 @@ given file (report RP0402 must not be disabled)'} args = '%r (%s)' % (modname, ex) else: args = repr(modname) - self.add_message("F0401", args=args, node=importnode) + self.add_message("import-error", args=args, node=importnode) def _check_relative_import(self, modnode, importnode, importedmodnode, importedasname): """check relative import. node is either an Import or From node, modname the imported module name. """ - if 'W0403' not in self.active_msgs: + if not self.linter.is_message_enabled('relative-import'): return if importedmodnode.file is None: return False # built-in module @@ -287,7 +287,7 @@ given file (report RP0402 must not be disabled)'} return False if importedmodnode.name != importedasname: # this must be a relative import... - self.add_message('W0403', args=(importedasname, importedmodnode.name), + self.add_message('relative-import', args=(importedasname, importedmodnode.name), node=importnode) def _add_imported_module(self, node, importedmodname): @@ -296,7 +296,7 @@ given file (report RP0402 must not be disabled)'} context_name = node.root().name if context_name == importedmodname: # module importing itself ! - self.add_message('W0406', node=node) + self.add_message('import-self', node=node) elif not is_standard_module(importedmodname): # handle dependencies importedmodnames = self.stats['dependencies'].setdefault( @@ -312,11 +312,11 @@ given file (report RP0402 must not be disabled)'} """check if the module is deprecated""" for mod_name in self.config.deprecated_modules: if mod_path == mod_name or mod_path.startswith(mod_name + '.'): - self.add_message('W0402', node=node, args=mod_path) + self.add_message('deprecated-module', node=node, args=mod_path) def _check_reimport(self, node, name, basename=None, level=None): """check if the import is necessary (i.e. not already done)""" - if 'W0404' not in self.active_msgs: + if not self.linter.is_message_enabled('reimported'): return frame = node.frame() root = node.root() @@ -326,7 +326,7 @@ given file (report RP0402 must not be disabled)'} for context, level in contexts: first = get_first_import(node, context, name, basename, level) if first is not None: - self.add_message('W0404', node=node, + self.add_message('reimported', node=node, args=(name, first.fromlineno)) -- cgit v1.2.1 From f3eb7127070aae0c99540c76be3c13bead6f6649 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 19:55:32 +0200 Subject: Only emit symbolic warnings from the stdlib checker. --- checkers/stdlib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checkers/stdlib.py b/checkers/stdlib.py index 8cb78f4..a1c3133 100644 --- a/checkers/stdlib.py +++ b/checkers/stdlib.py @@ -42,7 +42,7 @@ class OpenModeChecker(BaseChecker): 'See http://docs.python.org/2/library/functions.html#open'), } - @utils.check_messages('W1501') + @utils.check_messages('bad-open-mode') def visit_callfunc(self, node): """Visit a CallFunc node.""" if hasattr(node, 'func'): @@ -59,7 +59,7 @@ class OpenModeChecker(BaseChecker): mode_arg = utils.safe_infer(mode_arg) if (isinstance(mode_arg, astroid.Const) and not re.match(_VALID_OPEN_MODE_REGEX, mode_arg.value)): - self.add_message('W1501', node=node, args=(mode_arg.value)) + self.add_message('bad-open-mode', node=node, args=(mode_arg.value)) except (utils.NoSuchArgumentError, TypeError): pass -- cgit v1.2.1 From b20dfed486dfdb2f2686c41cf2e91e024d522c28 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 08:32:06 +0200 Subject: Improve pragma handling to not detect pylint:* strings in non-comments. Closes #79 --- ChangeLog | 3 +++ lint.py | 8 +++----- test/messages/func_i0022.txt | 1 - 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6085467..ef8c42b 100644 --- a/ChangeLog +++ b/ChangeLog @@ -9,6 +9,9 @@ ChangeLog for Pylint * Extend the number of cases in which logging calls are detected. Fixes bitbucket issue #182. + * Improve pragma handling to not detect pylint:* strings in non-comments. + Fixes BitBucket issue #79. + * 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. diff --git a/lint.py b/lint.py index e69bc49..3e2e18c 100644 --- a/lint.py +++ b/lint.py @@ -464,12 +464,10 @@ warning, statement which respectively contain the number of errors / warnings\ """process tokens from the current module to search for module/block level options """ - comment = tokenize.COMMENT - newline = tokenize.NEWLINE - for (tok_type, _, start, _, line) in tokens: - if tok_type not in (comment, newline): + for (tok_type, content, start, _, _) in tokens: + if tok_type != tokenize.COMMENT: continue - match = OPTION_RGX.search(line) + match = OPTION_RGX.search(content) if match is None: continue if match.group(1).strip() == "disable-all" or \ diff --git a/test/messages/func_i0022.txt b/test/messages/func_i0022.txt index 477335c..c882cae 100644 --- a/test/messages/func_i0022.txt +++ b/test/messages/func_i0022.txt @@ -1,7 +1,6 @@ I: 5: Locally disabling C0103 I: 5: Suppressed 'invalid-name' (from line 5) I: 6: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" -I: 6: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" I: 6: Suppressed 'invalid-name' (from line 6) I: 9: Suppressed 'invalid-name' (from line 8) I: 12: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" -- cgit v1.2.1 From 57fee11b3f93200c47e26272b484252e1898ef71 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 10 Apr 2014 08:13:42 +0200 Subject: Fix naming of invocation types, astroid exports a lot more information, and class methods are actually bound. --- checkers/typecheck.py | 18 +++++---------- test/input/func_arguments.py | 48 ++++++++++++++++++++++++++++++++++++++++ test/messages/func_arguments.txt | 31 +++++++++++++++++--------- 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index a775e6c..0d4b13b 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -77,23 +77,15 @@ MSGS = { } def _determine_callable(callable_obj): - # Note that BoundMethod is a subclass of UnboundMethod (huh?), so must - # come first in this 'if..else'. + # Ordering is important, since BoundMethod is a subclass of UnboundMethod, + # and Function inherits Lambda. if isinstance(callable_obj, astroid.BoundMethod): # Bound methods have an extra implicit 'self' argument. - return callable_obj, 1, 'method' + return callable_obj, 1, callable_obj.type 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' + return callable_obj, 0, 'unbound method' elif isinstance(callable_obj, astroid.Function): - return callable_obj, 0, 'function' + return callable_obj, 0, callable_obj.type elif isinstance(callable_obj, astroid.Lambda): return callable_obj, 0, 'lambda' elif isinstance(callable_obj, astroid.Class): diff --git a/test/input/func_arguments.py b/test/input/func_arguments.py index a22ff8e..1c27ef2 100644 --- a/test/input/func_arguments.py +++ b/test/input/func_arguments.py @@ -1,6 +1,34 @@ """Test function argument checker""" __revision__ = '' +def decorator(fun): + """Decorator""" + return fun + + +class DemoClass(object): + """Test class for method invocations.""" + + @staticmethod + def static_method(arg): + """static method.""" + return arg + arg + + @classmethod + def class_method(cls, arg): + """class method""" + return arg + arg + + def method(self, arg): + """method.""" + return (self, arg) + + @decorator + def decorated_method(self, arg): + """decorated method.""" + return (self, arg) + + def function_1_arg(first_argument): """one argument function""" return first_argument @@ -33,3 +61,23 @@ function_default_arg(1, 4, coin="hello") function_default_arg(1, one=5) +# Remaining tests are for coverage of correct names in messages. +LAMBDA = lambda arg: 1 + +LAMBDA() + +def method_tests(): + """"Method invocations.""" + demo = DemoClass() + demo.static_method() + DemoClass.static_method() + + demo.class_method() + DemoClass.class_method() + + demo.method() + DemoClass.method(demo) + + demo.decorated_method() + DemoClass.decorated_method(demo) + diff --git a/test/messages/func_arguments.txt b/test/messages/func_arguments.txt index 642d212..e930b20 100644 --- a/test/messages/func_arguments.txt +++ b/test/messages/func_arguments.txt @@ -1,11 +1,20 @@ -E: 18: No value for argument 'first_argument' in function call -E: 19: Too many positional arguments for 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 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 +E: 46: No value for argument 'first_argument' in function call +E: 47: Too many positional arguments for function call +E: 49: No value for argument 'third_argument' in function call +E: 50: No value for argument 'first_argument' in function call +E: 50: No value for argument 'second_argument' in function call +E: 50: No value for argument 'third_argument' in function call +E: 52: Too many positional arguments for function call +E: 59: No value for argument 'first_argument' in function call +E: 59: Unexpected keyword argument 'bob' in function call +E: 60: Unexpected keyword argument 'coin' in function call +E: 62: Argument 'one' passed by position and keyword in function call +E: 67: No value for argument 'arg' in lambda call +E: 72:method_tests: No value for argument 'arg' in staticmethod call +E: 73:method_tests: No value for argument 'arg' in staticmethod call +E: 75:method_tests: No value for argument 'arg' in classmethod call +E: 76:method_tests: No value for argument 'arg' in classmethod call +E: 78:method_tests: No value for argument 'arg' in method call +E: 79:method_tests: No value for argument 'arg' in unbound method call +E: 81:method_tests: No value for argument 'arg' in method call +E: 82:method_tests: No value for argument 'arg' in unbound method call -- cgit v1.2.1 From 2661908945c68572c2bf91abcadb61bf56db4aa0 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 15 Apr 2014 18:48:39 +0200 Subject: [test utils] add walk method to CheckerTestCase --- testutils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/testutils.py b/testutils.py index 69ffde5..8d8064d 100644 --- a/testutils.py +++ b/testutils.py @@ -30,6 +30,7 @@ from cStringIO import StringIO from logilab.common import testlib from pylint import checkers +from pylint.utils import PyLintASTWalker from pylint.reporters import BaseReporter from pylint.interfaces import IReporter from pylint.lint import PyLinter @@ -201,6 +202,12 @@ class CheckerTestCase(testlib.TestCase): '\n'.join(repr(m) for m in got))) self.assertEqual(list(messages), got, msg) + def walk(self, node): + """recursive walk on the given node""" + walker = PyLintASTWalker(linter) + walker.add_checker(self.checker) + walker.walk(node) + # Init test_reporter = TestReporter() -- cgit v1.2.1 From 72f1d4f950bdd888aad2bfd1f70dcaa8c8d31dba Mon Sep 17 00:00:00 2001 From: Anthony Truchet Date: Fri, 11 Apr 2014 10:59:13 +0200 Subject: [variable checker] Fix unused import false positive with augment assignment. Closes #78 --- ChangeLog | 2 ++ checkers/variables.py | 6 +++++- test/unittest_checker_variables.py | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 test/unittest_checker_variables.py diff --git a/ChangeLog b/ChangeLog index bd24b97..30dbc07 100644 --- a/ChangeLog +++ b/ChangeLog @@ -87,6 +87,8 @@ ChangeLog for Pylint * Don't register the newstyle checker w/ python >= 3 + * Fix unused-import false positive w/ augment assignment (#78) + 2013-12-22 -- 1.1.0 * Add new check for use of deprecated pragma directives "pylint:disable-msg" diff --git a/checkers/variables.py b/checkers/variables.py index 469ecca..cdf6c22 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -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 @@ -242,6 +242,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' if not self.config.init_import and node.package: return for name, stmts in not_consumed.iteritems(): + if any(isinstance(stmt, astroid.AssName) + and isinstance(stmt.ass_type(), astroid.AugAssign) + for stmt in stmts): + continue stmt = stmts[0] if isinstance(stmt, astroid.Import): self.add_message('W0611', args=name, node=stmt) diff --git a/test/unittest_checker_variables.py b/test/unittest_checker_variables.py new file mode 100644 index 0000000..6b9b654 --- /dev/null +++ b/test/unittest_checker_variables.py @@ -0,0 +1,21 @@ +"""Unit tests for the variables checker.""" + +from astroid import test_utils +from pylint.checkers import variables +from pylint.testutils import CheckerTestCase + +class VariablesCheckerTC(CheckerTestCase): + + CHECKER_CLASS = variables.VariablesChecker + + def test_bitbucket_issue_78(self): + """ Issue 78 report a false positive for unused-module """ + module = test_utils.build_module(""" + from sys import path + path += ['stuff'] + def func(): + other = 1 + return len(other) + """) + with self.assertNoMessages(): + self.walk(module) -- cgit v1.2.1 From 053b33b2062e59cdd2a6d01544e63ecae666e355 Mon Sep 17 00:00:00 2001 From: Holger Peters Date: Fri, 11 Apr 2014 12:00:45 +0200 Subject: Make optparse a default for the ignored modules parameter --- checkers/typecheck.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 9a9f723..f504b5f 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -142,12 +142,13 @@ class should be ignored. A mixin class is detected if its name ends with \ "mixin" (case insensitive).'} ), ('ignored-modules', - {'default': ('numpy',), + {'default': ('optparse', 'numpy',), 'type': 'csv', 'metavar': '', 'help': 'List of module names for which member attributes \ should not be checked (useful for modules/projects where namespaces are \ -manipulated during runtime'}, +manipulated during runtime and thus extisting member attributes cannot be \ +deduced by static analysis'}, ), ('ignored-classes', {'default' : ('SQLObject',), @@ -245,9 +246,8 @@ accessed. Python regular expressions are accepted.'} continue if isinstance(owner, Instance) and owner.has_dynamic_getattr(): continue - # explicit skipping of optparse'Values class - if (owner.name == 'Values' and owner.root().name == 'optparse' - or owner.name in self.config.ignored_modules): + # explicit skipping of module member access + if owner.root().name in self.config.ignored_modules: continue missingattr.add((owner, name)) continue -- cgit v1.2.1 From 3e0cdd3973029f23e52db23bcd7db413834bc336 Mon Sep 17 00:00:00 2001 From: Holger Peters Date: Fri, 11 Apr 2014 12:02:18 +0200 Subject: Create a test for the TypeChecker --- test/test_type.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 test/test_type.py diff --git a/test/test_type.py b/test/test_type.py new file mode 100644 index 0000000..debfa98 --- /dev/null +++ b/test/test_type.py @@ -0,0 +1,29 @@ +"""Unittest for the type checker.""" + +import re + +from astroid import test_utils +from pylint.checkers import typecheck +from pylint.testutils import CheckerTestCase, Message, set_config + + +class TypeCheckerTest(CheckerTestCase): + CHECKER_CLASS = typecheck.TypeChecker + CONFIG = { + 'bad_names': set(), + } + +# @set_config(include_naming_hint=True, +# const_name_hint='CONSTANT') + def test_naming_hint_configured_hint(self): + const = test_utils.extract_node(""" + import argparse + argparse.THIS_does_not_EXIST + """) + with self.assertAddsMessages(Message('no-member', node=const, args=('Module', 'argparse', 'THIS_does_not_EXIST'))): + self.checker.visit_getattr(const) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() -- cgit v1.2.1 From eb6a123cad2d86dd91d8322c32ea7716e3dc9a5e Mon Sep 17 00:00:00 2001 From: Holger Peters Date: Fri, 11 Apr 2014 12:16:09 +0200 Subject: Add more tests for TypeChecker's getattr check and ignored_modules --- test/test_type.py | 52 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/test/test_type.py b/test/test_type.py index debfa98..7c65215 100644 --- a/test/test_type.py +++ b/test/test_type.py @@ -1,27 +1,53 @@ """Unittest for the type checker.""" -import re - from astroid import test_utils from pylint.checkers import typecheck from pylint.testutils import CheckerTestCase, Message, set_config - class TypeCheckerTest(CheckerTestCase): + "Tests for pylint.checkers.typecheck" CHECKER_CLASS = typecheck.TypeChecker - CONFIG = { - 'bad_names': set(), - } - -# @set_config(include_naming_hint=True, -# const_name_hint='CONSTANT') - def test_naming_hint_configured_hint(self): - const = test_utils.extract_node(""" + + def test_no_member_in_getattr(self): + """Make sure that a module attribute access is checked by pylint. + """ + + node = test_utils.extract_node(""" + import argparse + argparse.THIS_does_not_EXIST + """) + with self.assertAddsMessages( + Message( + 'no-member', + node=node, + args=('Module', 'argparse', 'THIS_does_not_EXIST'))): + self.checker.visit_getattr(node) + + def test_no_member_in_getattr_ignored(self): + """Make sure that a module attribute access check is omitted with a + module that is configured to be ignored. + """ + + node = test_utils.extract_node(""" + import optparse + optparse.THIS_does_not_EXIST + """) + with self.assertNoMessages(): + self.checker.visit_getattr(node) + + + @set_config(ignored_modules=('argparse',)) + def test_no_member_in_getattr_ignored_cust_config(self): + """Make sure that a module can be added to the ignored_modules list and + a no-member message will not be raised upon attribute access. + """ + + node = test_utils.extract_node(""" import argparse argparse.THIS_does_not_EXIST """) - with self.assertAddsMessages(Message('no-member', node=const, args=('Module', 'argparse', 'THIS_does_not_EXIST'))): - self.checker.visit_getattr(const) + with self.assertNoMessages(): + self.checker.visit_getattr(node) if __name__ == '__main__': -- cgit v1.2.1 From 16cf3596fca82978b326436201b869f1fcc92763 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 11:54:05 +0200 Subject: [test] strip output to avoid dumb errors --- testutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testutils.py b/testutils.py index 1658e10..69ffde5 100644 --- a/testutils.py +++ b/testutils.py @@ -256,7 +256,7 @@ class LintTestUsingModule(testlib.TestCase): self._test(tocheck) def _check_result(self, got): - self.assertMultiLineEqual(self._get_expected(), got) + self.assertMultiLineEqual(self._get_expected().strip(), got.strip()) def _test(self, tocheck): if INFO_TEST_RGX.match(self.module): @@ -272,7 +272,7 @@ class LintTestUsingModule(testlib.TestCase): print ex ex.__str__ = exception_str raise - self._check_result(self.linter.reporter.finalize()) + self._check_result(self.linter.reporter.finalize()) def _has_output(self): return not self.module.startswith('func_noerror_') -- cgit v1.2.1 From 93e8ce9fddbdd0efa6b94e7edc79b2bc739d69b3 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 11:55:57 +0200 Subject: use symbolic names in the newstyle checker --- checkers/newstyle.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/checkers/newstyle.py b/checkers/newstyle.py index e583126..88ed92d 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -67,19 +67,19 @@ class NewStyleConflictChecker(BaseChecker): # configuration options options = () - @check_messages('E1001', 'C1001') + @check_messages('slots-on-old-class', 'old-style-class') def visit_class(self, node): """check __slots__ usage """ if '__slots__' in node and not node.newstyle: - self.add_message('E1001', node=node) + self.add_message('slots-on-old-class', node=node) # The node type could be class, exception, metaclass, or # interface. Presumably, the non-class-type nodes would always # have an explicit base class anyway. if not node.bases and node.type == 'class': - self.add_message('C1001', node=node) + self.add_message('old-style-class', node=node) - @check_messages('W1001') + @check_messages('property-on-old-class') def visit_callfunc(self, node): """check property usage""" parent = node.parent.frame() @@ -88,9 +88,9 @@ class NewStyleConflictChecker(BaseChecker): isinstance(node.func, astroid.Name)): name = node.func.name if name == 'property': - self.add_message('W1001', node=node) + self.add_message('property-on-old-class', node=node) - @check_messages('E1002', 'E1003', 'E1004') + @check_messages('super-on-old-class', 'bad-super-call', 'missing-super-argument') def visit_function(self, node): """check use of super""" # ignore actual functions or method within a new style class @@ -108,7 +108,7 @@ class NewStyleConflictChecker(BaseChecker): call.func.name == 'super': if not klass.newstyle: # super should not be used on an old style class - self.add_message('E1002', node=node) + self.add_message('super-on-old-class', node=node) else: # super first arg should be the class if not call.args and sys.version_info[0] == 3: @@ -126,7 +126,7 @@ class NewStyleConflictChecker(BaseChecker): continue if klass is not supcls: - self.add_message('E1003', node=call, + self.add_message('bad-super-call', node=call, args=(call.args[0].name, )) -- cgit v1.2.1 From 1970026bfef39558de6871e4333967670ec7669f Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 12:23:00 +0200 Subject: Always register messages even if they may not be emitted. This avoid several problems: * error reported because the message is used in enable/disable * check_message doesn't work properly because message is erroneously considered as enabled (because check_message_id raise UnknownMessage) So, put them all in linter._messages, filtering out non-emitable message in list_messages. --- utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.py b/utils.py index e1cb3a8..804229a 100644 --- a/utils.py +++ b/utils.py @@ -239,7 +239,6 @@ class MessagesHandlerMixIn(object): chkid = msg.msgid[1:3] if not msg.may_be_emitted(): self._msgs_state[msg.msgid] = False - continue self._messages[msg.symbol] = msg self._alternative_names[msg.msgid] = msg for old_id, old_symbol in msg.old_names: @@ -537,6 +536,8 @@ class MessagesHandlerMixIn(object): """output full messages list documentation in ReST format""" msgs = sorted(self._messages.itervalues(), key=lambda msg: msg.msgid) for msg in msgs: + if not msg.may_be_emitted(): + continue print msg.format_help(checkerref=False) print @@ -661,7 +662,6 @@ class PyLintASTWalker(object): def _is_method_enabled(self, method): if not hasattr(method, 'checks_msgs'): return True - for msg_desc in method.checks_msgs: if self.linter.is_message_enabled(msg_desc): return True -- cgit v1.2.1 From c7fa2a46e61bd7b13177e5cbf16d8ff5c95a3685 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 12:24:08 +0200 Subject: restore newstyle checker, we still want (part of) the super call checks. Rely on check_messages to disable methods with useless messages --- checkers/newstyle.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/checkers/newstyle.py b/checkers/newstyle.py index 88ed92d..8bdbb25 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -26,10 +26,12 @@ from pylint.checkers.utils import check_messages MSGS = { 'E1001': ('Use of __slots__ on an old style class', 'slots-on-old-class', - 'Used when an old style class uses the __slots__ attribute.'), + 'Used when an old style class uses the __slots__ attribute.', + {'maxversion': (3, 0)}), 'E1002': ('Use of super on an old style class', 'super-on-old-class', - 'Used when an old style class uses the super builtin.'), + 'Used when an old style class uses the super builtin.', + {'maxversion': (3, 0)}), 'E1003': ('Bad first argument %r given to super()', 'bad-super-call', 'Used when another argument than the current class is given as \ @@ -37,16 +39,19 @@ MSGS = { 'E1004': ('Missing argument to super()', 'missing-super-argument', 'Used when the super builtin didn\'t receive an \ - argument on Python 2'), + argument.', + {'maxversion': (3, 0)}), 'W1001': ('Use of "property" on an old style class', 'property-on-old-class', 'Used when PyLint detect the use of the builtin "property" \ on an old style class while this is relying on new style \ - classes features'), + classes features.', + {'maxversion': (3, 0)}), 'C1001': ('Old-style class defined.', 'old-style-class', 'Used when a class is defined that does not inherit from another' - 'class and does not inherit explicitly from "object".') + 'class and does not inherit explicitly from "object".', + {'maxversion': (3, 0)}) } @@ -132,6 +137,4 @@ class NewStyleConflictChecker(BaseChecker): def register(linter): """required method to auto register this checker """ - import sys - if sys.version_info < (3, 0): - linter.register_checker(NewStyleConflictChecker(linter)) + linter.register_checker(NewStyleConflictChecker(linter)) -- cgit v1.2.1 From 7440b7e7e8b2ecdc546a2188f330253ab853f024 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 13:09:55 +0200 Subject: adapt list of deprecated modules to python 3 and update test accordingly --- checkers/imports.py | 8 +++++++- test/input/func_deprecated_module_py30.py | 12 ++++++++++++ test/input/func_deprecated_module_py_30.py | 12 ++++++++++++ test/input/func_w0403.py | 12 ------------ test/messages/func_deprecated_module_py30.txt | 2 ++ test/messages/func_deprecated_module_py_30.txt | 2 ++ test/messages/func_w0403_py29.txt | 2 -- test/messages/func_w0403_py30.txt | 2 -- 8 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 test/input/func_deprecated_module_py30.py create mode 100644 test/input/func_deprecated_module_py_30.py delete mode 100644 test/input/func_w0403.py create mode 100644 test/messages/func_deprecated_module_py30.txt create mode 100644 test/messages/func_deprecated_module_py_30.txt delete mode 100644 test/messages/func_w0403_py29.txt delete mode 100644 test/messages/func_w0403_py30.txt diff --git a/checkers/imports.py b/checkers/imports.py index fa47edc..1df2b3a 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -15,6 +15,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """imports checkers for Python code""" +import sys + from logilab.common.graph import get_cycles, DotBackend from logilab.common.modutils import get_module_part, is_standard_module from logilab.common.ureports import VerbatimText, Paragraph @@ -165,8 +167,12 @@ class ImportsChecker(BaseChecker): msgs = MSGS priority = -2 + if sys.version_info < (3,): + deprecated_modules = ('regsub', 'TERMIOS', 'Bastion', 'rexec') + else: + deprecated_modules = ('stringprep', 'optparse') options = (('deprecated-modules', - {'default' : ('regsub', 'TERMIOS', 'Bastion', 'rexec'), + {'default' : deprecated_modules, 'type' : 'csv', 'metavar' : '', 'help' : 'Deprecated modules which should not be used, \ diff --git a/test/input/func_deprecated_module_py30.py b/test/input/func_deprecated_module_py30.py new file mode 100644 index 0000000..cadbac6 --- /dev/null +++ b/test/input/func_deprecated_module_py30.py @@ -0,0 +1,12 @@ +"""test deprecated module +""" + +__revision__ = 0 + + +if __revision__: + import optparse + print optparse + # false positive (#10061) + import stringfile + print stringfile diff --git a/test/input/func_deprecated_module_py_30.py b/test/input/func_deprecated_module_py_30.py new file mode 100644 index 0000000..89d362d --- /dev/null +++ b/test/input/func_deprecated_module_py_30.py @@ -0,0 +1,12 @@ +"""test deprecated module +""" + +__revision__ = 0 + + +if __revision__: + import Bastion + print Bastion + # false positive (#10061) + import stringfile + print stringfile diff --git a/test/input/func_w0403.py b/test/input/func_w0403.py deleted file mode 100644 index 89d362d..0000000 --- a/test/input/func_w0403.py +++ /dev/null @@ -1,12 +0,0 @@ -"""test deprecated module -""" - -__revision__ = 0 - - -if __revision__: - import Bastion - print Bastion - # false positive (#10061) - import stringfile - print stringfile diff --git a/test/messages/func_deprecated_module_py30.txt b/test/messages/func_deprecated_module_py30.txt new file mode 100644 index 0000000..d4c88cf --- /dev/null +++ b/test/messages/func_deprecated_module_py30.txt @@ -0,0 +1,2 @@ +F: 11: Unable to import 'stringfile' +W: 8: Uses of a deprecated module 'optparse' diff --git a/test/messages/func_deprecated_module_py_30.txt b/test/messages/func_deprecated_module_py_30.txt new file mode 100644 index 0000000..70679ec --- /dev/null +++ b/test/messages/func_deprecated_module_py_30.txt @@ -0,0 +1,2 @@ +F: 11: Unable to import 'stringfile' +W: 8: Uses of a deprecated module 'Bastion' diff --git a/test/messages/func_w0403_py29.txt b/test/messages/func_w0403_py29.txt deleted file mode 100644 index 70679ec..0000000 --- a/test/messages/func_w0403_py29.txt +++ /dev/null @@ -1,2 +0,0 @@ -F: 11: Unable to import 'stringfile' -W: 8: Uses of a deprecated module 'Bastion' diff --git a/test/messages/func_w0403_py30.txt b/test/messages/func_w0403_py30.txt deleted file mode 100644 index fc5da23..0000000 --- a/test/messages/func_w0403_py30.txt +++ /dev/null @@ -1,2 +0,0 @@ -F: 8: Unable to import 'Bastion' -F: 11: Unable to import 'stringfile' -- cgit v1.2.1 From b4db94d713c6301a471a89a9254ea157d5af9fcd Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 12:56:10 +0200 Subject: delete trailing whitespaces --- checkers/exceptions.py | 5 +++-- test/test_func.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 30b3fa8..e3ecff9 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -134,7 +134,7 @@ class ExceptionsChecker(BaseChecker): ), ) - @check_messages('raising-string', 'nonstandard-exception', 'raising-bad-type', + @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""" @@ -211,7 +211,8 @@ class ExceptionsChecker(BaseChecker): self.add_message('unpacking-in-except', node=node) - @check_messages('bare-except', 'broad-except', 'pointless-except', 'binary-op-exception', 'bad-except-order', + @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""" diff --git a/test/test_func.py b/test/test_func.py index 807878a..47862fc 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -78,7 +78,7 @@ class LintBuiltinModuleTest(LintTestUsingModule): def gen_tests(filter_rgx): if UPDATE: - callbacks = [cb_test_gen(LintTestUpdate)] + callbacks = [cb_test_gen(LintTestUpdate)] else: callbacks = [cb_test_gen(LintTestUsingModule)] if not MODULES_ONLY: -- cgit v1.2.1 From 61a8226c84e4c09c9e60b73514b21767f7dda13c Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 12:59:18 +0200 Subject: update test_func exhaustivity testing * skip messages that can not be emitted * use set instead of list * properly mark max version to some messages and drop now (almost) useless messages list --- checkers/exceptions.py | 3 ++- checkers/imports.py | 3 ++- test/test_func.py | 25 ++++++++----------------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index e3ecff9..64aca5b 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -94,7 +94,8 @@ MSGS = { 'W0710': ('Exception doesn\'t inherit from standard "Exception" class', 'nonstandard-exception', 'Used when a custom exception class is raised but doesn\'t \ - inherit from the builtin "Exception" class.'), + inherit from the builtin "Exception" class.', + {'maxversion': (3, 0)}), 'W0711': ('Exception to catch is the result of a binary "%s" operation', 'binary-op-exception', 'Used when the exception to catch is of the form \ diff --git a/checkers/imports.py b/checkers/imports.py index 1df2b3a..8b73c6f 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -150,7 +150,8 @@ MSGS = { 'W0410': ('__future__ import is not the first non docstring statement', 'misplaced-future', 'Python 2.5 and greater require __future__ import to be the \ - first non docstring statement in the module.'), + first non docstring statement in the module.', + {'maxversion': (3, 0)}), } class ImportsChecker(BaseChecker): diff --git a/test/test_func.py b/test/test_func.py index 47862fc..63c585f 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -47,27 +47,18 @@ class TestTests(testlib.TestCase): @testlib.tag('coverage') def test_exhaustivity(self): # skip fatal messages - todo = [msg.msgid for msg in linter.messages if msg.msgid[0] != 'F'] + not_tested = set(msg.msgid for msg in linter.messages + if msg.msgid[0] != 'F' and msg.may_be_emitted()) for msgid in test_reporter.message_ids: try: - todo.remove(msgid) - except ValueError: + not_tested.remove(msgid) + except KeyError: continue - todo.sort() + not_tested -= set(('I0001',)) if PY3K: - 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']) + not_tested.remove('W0403') # relative-import + self.assertFalse(not_tested) + class LintBuiltinModuleTest(LintTestUsingModule): output = join(MSG_DIR, 'builtin_module.txt') -- cgit v1.2.1 From 1427610034568644ab428a539bfaa10742f1d74d Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Mon, 14 Apr 2014 12:24:24 +0200 Subject: fix test for python 3 --- test/messages/func___name___access_py30.txt | 3 +++ test/messages/func_newstyle___slots___py30.txt | 1 - test/messages/func_newstyle_property_py30.txt | 1 - test/messages/func_newstyle_super_py30.txt | 1 - test/messages/func_typecheck_getattr_py30.txt | 9 +++++++++ test/messages/func_w0231_py30.txt | 3 +++ 6 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 test/messages/func___name___access_py30.txt create mode 100644 test/messages/func_typecheck_getattr_py30.txt create mode 100644 test/messages/func_w0231_py30.txt diff --git a/test/messages/func___name___access_py30.txt b/test/messages/func___name___access_py30.txt new file mode 100644 index 0000000..51aeeaa --- /dev/null +++ b/test/messages/func___name___access_py30.txt @@ -0,0 +1,3 @@ +E: 11:Aaaa.__init__: Instance of 'Aaaa' has no '__name__' member +E: 21:NewClass.__init__: Instance of 'NewClass' has no '__name__' member + diff --git a/test/messages/func_newstyle___slots___py30.txt b/test/messages/func_newstyle___slots___py30.txt index 0db581b..e69de29 100644 --- a/test/messages/func_newstyle___slots___py30.txt +++ b/test/messages/func_newstyle___slots___py30.txt @@ -1 +0,0 @@ -C: 10:HaNonNonNon: Old-style class defined. diff --git a/test/messages/func_newstyle_property_py30.txt b/test/messages/func_newstyle_property_py30.txt index b1d46a5..e69de29 100644 --- a/test/messages/func_newstyle_property_py30.txt +++ b/test/messages/func_newstyle_property_py30.txt @@ -1 +0,0 @@ -C: 14:HaNonNonNon: Old-style class defined. diff --git a/test/messages/func_newstyle_super_py30.txt b/test/messages/func_newstyle_super_py30.txt index 246d265..b1ee6cd 100644 --- a/test/messages/func_newstyle_super_py30.txt +++ b/test/messages/func_newstyle_super_py30.txt @@ -1,4 +1,3 @@ -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_typecheck_getattr_py30.txt b/test/messages/func_typecheck_getattr_py30.txt new file mode 100644 index 0000000..b6bf150 --- /dev/null +++ b/test/messages/func_typecheck_getattr_py30.txt @@ -0,0 +1,9 @@ +E: 25:Client.__init__: Class 'Provider' has no 'cattribute' member +E: 35:Client.use_method: Instance of 'Provider' has no 'hophophop' member +E: 40:Client.use_attr: Instance of 'Provider' has no 'attribute' member +E: 52:Client.test_bt_types: Instance of 'list' has no 'apppend' member +E: 54:Client.test_bt_types: Instance of 'dict' has no 'set' member +E: 56:Client.test_bt_types: Instance of 'tuple' has no 'append' member +E: 58:Client.test_bt_types: Instance of 'str' has no 'loower' member +E: 62:Client.test_bt_types: Instance of 'int' has no 'whatever' member +E: 66: Instance of 'int' has no 'lower' member (but some types could not be inferred) diff --git a/test/messages/func_w0231_py30.txt b/test/messages/func_w0231_py30.txt new file mode 100644 index 0000000..44e4f11 --- /dev/null +++ b/test/messages/func_w0231_py30.txt @@ -0,0 +1,3 @@ +W: 19:CCCC: Class has no __init__ method +W: 26:ZZZZ.__init__: __init__ method from base class 'BBBB' is not called +W: 59:AssignedInit.__init__: __init__ method from base class 'NewStyleC' is not called -- cgit v1.2.1 From ea2e75a77c1a5357863b8ff495cb028c99ecc574 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 14 Apr 2014 15:30:37 +0300 Subject: Fix Python < 2.7 compatibility. --- checkers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/base.py b/checkers/base.py index 497aa40..ac58177 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -59,7 +59,7 @@ if sys.version_info < (3, 0): BAD_FUNCTIONS.append('file') # Name categories that are always consistent with all naming conventions. -EXEMPT_NAME_CATEGORIES = {'exempt', 'ignore'} +EXEMPT_NAME_CATEGORIES = set(('exempt', 'ignore')) del re -- cgit v1.2.1 From 7678723873ca872d9f92032fc7cb8cc93709ad9a Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 14 Apr 2014 16:39:47 +0300 Subject: Fix tests for < Python 2.7. --- test/input/func_format.py | 108 ------------------------------------ test/input/func_format_py27.py | 108 ++++++++++++++++++++++++++++++++++++ test/input/func_format_py_27.py | 103 ++++++++++++++++++++++++++++++++++ test/messages/func_format.txt | 46 --------------- test/messages/func_format_py27.txt | 46 +++++++++++++++ test/messages/func_format_py_27.txt | 45 +++++++++++++++ test/unittest_checkers_utils.py | 6 +- 7 files changed, 305 insertions(+), 157 deletions(-) delete mode 100644 test/input/func_format.py create mode 100644 test/input/func_format_py27.py create mode 100644 test/input/func_format_py_27.py delete mode 100644 test/messages/func_format.txt create mode 100644 test/messages/func_format_py27.txt create mode 100644 test/messages/func_format_py_27.txt diff --git a/test/input/func_format.py b/test/input/func_format.py deleted file mode 100644 index 828a180..0000000 --- a/test/input/func_format.py +++ /dev/null @@ -1,108 +0,0 @@ -# pylint:disable=C0103,W0104,W0105 -"""Check format -""" -__revision__ = '' - -notpreceded= 1 -notfollowed =1 -notfollowed <=1 - -correct = 1 -correct >= 1 - -def func(arg, arg2): - """test named argument - """ - func(arg=arg+1, - arg2=arg2-arg) - -aaaa,bbbb = 1,2 -aaaa |= bbbb -aaaa &= bbbb - - -if aaaa: pass -else: - aaaa,bbbb = 1,2 - aaaa,bbbb = bbbb,aaaa - -bbbb = (1,2,3) - -aaaa = bbbb[1:] -aaaa = bbbb[:1] -aaaa = bbbb[:] - -aaaa = {aaaa:bbbb} - - -# allclose(x,y) uses |x-y|ist genau gleich -yo+=4 -""" -html2 = """ -yo+=4 -""" - -func('''Hello -''', 0) - -assert boo <= 10, "Note is %.2f. Either you cheated, or pylint's \ -broken!" % boo - -def _gc_debug(gcc): - """bad format undetected w/ py2.5""" - ocount = {} - for obj in gcc.get_objects(): - try: - ocount[obj.__class__]+= 1 - except KeyError: - ocount[obj.__class__]=1 - except AttributeError: - pass - -def hop(context): - """multi-lines string""" - return [''' -%(send)s%(send)s''' % context, - ''' -%(cancel)s%(cancel)s''' % context, - ] -titreprojet = '\ -Drapeau vert\ -%s' % aaaa - -with open('a') as a, open('b') as b: - pass - -with open('a') as a, open('b') as b: pass # multiple-statements - -# Well-formatted try-except-finally block. -try: - pass -except IOError, e: - print e -finally: - pass - -try: - pass -except IOError, e: - print e -finally: pass # multiple-statements - -# This is not allowed by the default configuration. -if True: print False diff --git a/test/input/func_format_py27.py b/test/input/func_format_py27.py new file mode 100644 index 0000000..828a180 --- /dev/null +++ b/test/input/func_format_py27.py @@ -0,0 +1,108 @@ +# pylint:disable=C0103,W0104,W0105 +"""Check format +""" +__revision__ = '' + +notpreceded= 1 +notfollowed =1 +notfollowed <=1 + +correct = 1 +correct >= 1 + +def func(arg, arg2): + """test named argument + """ + func(arg=arg+1, + arg2=arg2-arg) + +aaaa,bbbb = 1,2 +aaaa |= bbbb +aaaa &= bbbb + + +if aaaa: pass +else: + aaaa,bbbb = 1,2 + aaaa,bbbb = bbbb,aaaa + +bbbb = (1,2,3) + +aaaa = bbbb[1:] +aaaa = bbbb[:1] +aaaa = bbbb[:] + +aaaa = {aaaa:bbbb} + + +# allclose(x,y) uses |x-y|ist genau gleich +yo+=4 +""" +html2 = """ +yo+=4 +""" + +func('''Hello +''', 0) + +assert boo <= 10, "Note is %.2f. Either you cheated, or pylint's \ +broken!" % boo + +def _gc_debug(gcc): + """bad format undetected w/ py2.5""" + ocount = {} + for obj in gcc.get_objects(): + try: + ocount[obj.__class__]+= 1 + except KeyError: + ocount[obj.__class__]=1 + except AttributeError: + pass + +def hop(context): + """multi-lines string""" + return [''' +%(send)s%(send)s''' % context, + ''' +%(cancel)s%(cancel)s''' % context, + ] +titreprojet = '\ +Drapeau vert\ +%s' % aaaa + +with open('a') as a, open('b') as b: + pass + +with open('a') as a, open('b') as b: pass # multiple-statements + +# Well-formatted try-except-finally block. +try: + pass +except IOError, e: + print e +finally: + pass + +try: + pass +except IOError, e: + print e +finally: pass # multiple-statements + +# This is not allowed by the default configuration. +if True: print False diff --git a/test/input/func_format_py_27.py b/test/input/func_format_py_27.py new file mode 100644 index 0000000..f062f27 --- /dev/null +++ b/test/input/func_format_py_27.py @@ -0,0 +1,103 @@ +# pylint:disable=C0103,W0104,W0105 +"""Check format +""" +__revision__ = '' + +notpreceded= 1 +notfollowed =1 +notfollowed <=1 + +correct = 1 +correct >= 1 + +def func(arg, arg2): + """test named argument + """ + func(arg=arg+1, + arg2=arg2-arg) + +aaaa,bbbb = 1,2 +aaaa |= bbbb +aaaa &= bbbb + + +if aaaa: pass +else: + aaaa,bbbb = 1,2 + aaaa,bbbb = bbbb,aaaa + +bbbb = (1,2,3) + +aaaa = bbbb[1:] +aaaa = bbbb[:1] +aaaa = bbbb[:] + +aaaa = {aaaa:bbbb} + + +# allclose(x,y) uses |x-y|ist genau gleich +yo+=4 +""" +html2 = """ +yo+=4 +""" + +func('''Hello +''', 0) + +assert boo <= 10, "Note is %.2f. Either you cheated, or pylint's \ +broken!" % boo + +def _gc_debug(gcc): + """bad format undetected w/ py2.5""" + ocount = {} + for obj in gcc.get_objects(): + try: + ocount[obj.__class__]+= 1 + except KeyError: + ocount[obj.__class__]=1 + except AttributeError: + pass + +def hop(context): + """multi-lines string""" + return [''' +%(send)s%(send)s''' % context, + ''' +%(cancel)s%(cancel)s''' % context, + ] +titreprojet = '\ +Drapeau vert\ +%s' % aaaa + +# Well-formatted try-except-finally block. +try: + pass +except IOError, e: + print e +finally: + pass + +try: + pass +except IOError, e: + print e +finally: pass # multiple-statements + +# This is not allowed by the default configuration. +if True: print False diff --git a/test/messages/func_format.txt b/test/messages/func_format.txt deleted file mode 100644 index ce5df81..0000000 --- a/test/messages/func_format.txt +++ /dev/null @@ -1,46 +0,0 @@ -C: 6: Exactly one space required before assignment -notpreceded= 1 - ^ -C: 7: Exactly one space required after assignment -notfollowed =1 - ^ -C: 8: Exactly one space required after comparison -notfollowed <=1 - ^^ -C: 19: Exactly one space required after comma -aaaa,bbbb = 1,2 - ^ -C: 19: Exactly one space required after comma -aaaa,bbbb = 1,2 - ^ -C: 24: More than one statement on a single line -C: 26: Exactly one space required after comma - aaaa,bbbb = 1,2 - ^ -C: 26: Exactly one space required after comma - aaaa,bbbb = 1,2 - ^ -C: 27: Exactly one space required after comma - aaaa,bbbb = bbbb,aaaa - ^ -C: 27: Exactly one space required after comma - aaaa,bbbb = bbbb,aaaa - ^ -C: 29: Exactly one space required after comma -bbbb = (1,2,3) - ^ -C: 29: Exactly one space required after comma -bbbb = (1,2,3) - ^ -C: 51: Exactly one space required before assignment - funky= funky+2 - ^ -C: 71: Exactly one space required before assignment - ocount[obj.__class__]+= 1 - ^^ -C: 73: Exactly one space required around assignment - ocount[obj.__class__]=1 - ^ -C: 91: More than one statement on a single line -C:105: More than one statement on a single line -C:108: More than one statement on a single line diff --git a/test/messages/func_format_py27.txt b/test/messages/func_format_py27.txt new file mode 100644 index 0000000..ce5df81 --- /dev/null +++ b/test/messages/func_format_py27.txt @@ -0,0 +1,46 @@ +C: 6: Exactly one space required before assignment +notpreceded= 1 + ^ +C: 7: Exactly one space required after assignment +notfollowed =1 + ^ +C: 8: Exactly one space required after comparison +notfollowed <=1 + ^^ +C: 19: Exactly one space required after comma +aaaa,bbbb = 1,2 + ^ +C: 19: Exactly one space required after comma +aaaa,bbbb = 1,2 + ^ +C: 24: More than one statement on a single line +C: 26: Exactly one space required after comma + aaaa,bbbb = 1,2 + ^ +C: 26: Exactly one space required after comma + aaaa,bbbb = 1,2 + ^ +C: 27: Exactly one space required after comma + aaaa,bbbb = bbbb,aaaa + ^ +C: 27: Exactly one space required after comma + aaaa,bbbb = bbbb,aaaa + ^ +C: 29: Exactly one space required after comma +bbbb = (1,2,3) + ^ +C: 29: Exactly one space required after comma +bbbb = (1,2,3) + ^ +C: 51: Exactly one space required before assignment + funky= funky+2 + ^ +C: 71: Exactly one space required before assignment + ocount[obj.__class__]+= 1 + ^^ +C: 73: Exactly one space required around assignment + ocount[obj.__class__]=1 + ^ +C: 91: More than one statement on a single line +C:105: More than one statement on a single line +C:108: More than one statement on a single line diff --git a/test/messages/func_format_py_27.txt b/test/messages/func_format_py_27.txt new file mode 100644 index 0000000..20cba32 --- /dev/null +++ b/test/messages/func_format_py_27.txt @@ -0,0 +1,45 @@ +C: 6: Exactly one space required before assignment +notpreceded= 1 + ^ +C: 7: Exactly one space required after assignment +notfollowed =1 + ^ +C: 8: Exactly one space required after comparison +notfollowed <=1 + ^^ +C: 19: Exactly one space required after comma +aaaa,bbbb = 1,2 + ^ +C: 19: Exactly one space required after comma +aaaa,bbbb = 1,2 + ^ +C: 24: More than one statement on a single line +C: 26: Exactly one space required after comma + aaaa,bbbb = 1,2 + ^ +C: 26: Exactly one space required after comma + aaaa,bbbb = 1,2 + ^ +C: 27: Exactly one space required after comma + aaaa,bbbb = bbbb,aaaa + ^ +C: 27: Exactly one space required after comma + aaaa,bbbb = bbbb,aaaa + ^ +C: 29: Exactly one space required after comma +bbbb = (1,2,3) + ^ +C: 29: Exactly one space required after comma +bbbb = (1,2,3) + ^ +C: 51: Exactly one space required before assignment + funky= funky+2 + ^ +C: 71: Exactly one space required before assignment + ocount[obj.__class__]+= 1 + ^^ +C: 73: Exactly one space required around assignment + ocount[obj.__class__]=1 + ^ +C:100: More than one statement on a single line +C:103: More than one statement on a single line diff --git a/test/unittest_checkers_utils.py b/test/unittest_checkers_utils.py index d8ebd3b..72108d7 100644 --- a/test/unittest_checkers_utils.py +++ b/test/unittest_checkers_utils.py @@ -18,10 +18,10 @@ __revision__ = '$Id: unittest_checkers_utils.py,v 1.6 2005-11-02 09:22:07 syt Exp $' -import unittest import sys from astroid import test_utils +from logilab.common.testlib import TestCase, unittest_main from pylint.checkers import utils try: @@ -29,7 +29,7 @@ try: except AttributeError: __builtins__['mybuiltin'] = 2 -class UtilsTC(unittest.TestCase): +class UtilsTC(TestCase): ## def test_is_native_builtin(self): ## self.assertEqual(utils.is_native_builtin('min'), True) @@ -70,5 +70,5 @@ class UtilsTC(unittest.TestCase): self.assertEqual(name.name, 'a') if __name__ == '__main__': - unittest.main() + unittest_main() -- cgit v1.2.1 From e3f6b8eb794913438c2ecae6c2cba54185bd83dd Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 14 Apr 2014 17:35:14 +0300 Subject: Fix test for Python < 2.7. --- test/input/func_unpacking_non_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/input/func_unpacking_non_sequence.py b/test/input/func_unpacking_non_sequence.py index cd35328..469add7 100644 --- a/test/input/func_unpacking_non_sequence.py +++ b/test/input/func_unpacking_non_sequence.py @@ -2,7 +2,7 @@ # pylint: disable=too-few-public-methods, invalid-name -from sys import exit as nonseq_func +from os import rename as nonseq_func from input.unpacking import nonseq __revision__ = 0 -- cgit v1.2.1 From dba33a5a25bddad1ab508c25412da8145106017d Mon Sep 17 00:00:00 2001 From: Manuel V?zquez Acosta Date: Mon, 14 Apr 2014 16:07:58 -0400 Subject: Removes overloaded PYTHONPATH. --- epylint.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/epylint.py b/epylint.py index cd8dd26..87ad793 100755 --- a/epylint.py +++ b/epylint.py @@ -54,13 +54,7 @@ def _get_env(): '''Extracts the environment PYTHONPATH and appends the current sys.path to those.''' env = dict(os.environ) - pythonpath = env.get('PYTHONPATH', '') - currentpaths = os.pathsep.join(sys.path) - if pythonpath: - pythonpath += os.pathsep + currentpaths - else: - pythonpath = currentpaths - env['PYTHONPATH'] = pythonpath + env['PYTHONPATH'] = os.pathsep.join(sys.path) return env def lint(filename, options=None): -- cgit v1.2.1 From 657671186dd1087cc3b39f0bac4d2fc0ac184088 Mon Sep 17 00:00:00 2001 From: Manuel V?zquez Acosta Date: Mon, 14 Apr 2014 16:25:34 -0400 Subject: Fix merge error. Closes #133. --- ChangeLog | 6 +++--- epylint.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index ef8c42b..bd24b97 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,9 @@ ChangeLog for Pylint -- + * Pass the current python paths to pylint process when invoked via + epylint. Fixes BitBucket issue #133. + * Add -i / --include-ids and -s / --symbols back as completely ignored options. Fixes BitBucket issue #180. @@ -1390,6 +1393,3 @@ ChangeLog for Pylint 2003-05-19 -- 0.1 * initial release - - - diff --git a/epylint.py b/epylint.py index 87ad793..2e70ae4 100755 --- a/epylint.py +++ b/epylint.py @@ -87,8 +87,9 @@ 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, env=_get_env(), universal_newlines=True) -- cgit v1.2.1 From 6735dfd5dfbe31ecf8339b8004a1d63043cf327e Mon Sep 17 00:00:00 2001 From: Manuel V?zquez Acosta Date: Mon, 14 Apr 2014 16:57:25 -0400 Subject: More fixes to merge. Sorry. I'm not used to hg merges. --- epylint.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/epylint.py b/epylint.py index 2e70ae4..4453662 100755 --- a/epylint.py +++ b/epylint.py @@ -93,25 +93,11 @@ def lint(filename, options=None): process = Popen(cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), 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[WE])(?P.*?)\]") - - 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: @@ -182,7 +168,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__': -- cgit v1.2.1 From ac73444b00095d131433338214562c293238b002 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 15 Apr 2014 08:55:50 +0200 Subject: fix fsf address --- epylint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epylint.py b/epylint.py index 4453662..24e9e95 100755 --- a/epylint.py +++ b/epylint.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. """Emacs and Flymake compatible Pylint. This script is for integration with emacs and is compatible with flymake mode. -- cgit v1.2.1 From 709fdbcf823704153e785a1faf48228d93d36971 Mon Sep 17 00:00:00 2001 From: Holger Peters Date: Tue, 15 Apr 2014 14:15:11 +0200 Subject: Removing numpy from defaults for ignored modules in type checker --- checkers/typecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index f504b5f..4cbcdfd 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -142,7 +142,7 @@ class should be ignored. A mixin class is detected if its name ends with \ "mixin" (case insensitive).'} ), ('ignored-modules', - {'default': ('optparse', 'numpy',), + {'default': ('optparse',), 'type': 'csv', 'metavar': '', 'help': 'List of module names for which member attributes \ -- cgit v1.2.1 From 5a48992e582b5c6dacdca456c7280c74e1f23603 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 15 Apr 2014 18:49:50 +0200 Subject: various cleanups --- checkers/classes.py | 12 +++++++----- checkers/variables.py | 13 +++++++------ testutils.py | 3 ++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/checkers/classes.py b/checkers/classes.py index fc2e2d6..6dd5df0 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -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 @@ -16,7 +16,9 @@ """classes checker for Python code """ from __future__ import generators + import sys + import astroid from astroid import YES, Instance, are_exclusive, AssAttr from astroid.bases import Generator @@ -518,8 +520,8 @@ a metaclass class method.'} for attr, nodes in accessed.iteritems(): # deactivate "except doesn't do anything", that's expected # pylint: disable=W0704 - # is it a class attribute ? try: + # is it a class attribute ? node.local_attr(attr) # yes, stop here continue @@ -547,8 +549,8 @@ a metaclass class method.'} for _node in nodes: if _node.frame() is frame and _node.fromlineno < lno \ and not are_exclusive(_node.statement(), defstmt, ('AttributeError', 'Exception', 'BaseException')): - self.add_message('access-member-before-definition', node=_node, - args=(attr, lno)) + self.add_message('access-member-before-definition', + node=_node, args=(attr, lno)) def _check_first_arg_for_type(self, node, metaclass=0): """check the name of first argument, expect: @@ -685,7 +687,7 @@ a metaclass class method.'} """check that the __init__ method call super or ancestors'__init__ method """ - if (not self.linter.is_message_enabled('super-init-not-called') and + if (not self.linter.is_message_enabled('super-init-not-called') and not self.linter.is_message_enabled('non-parent-init-called')): return klass_node = node.parent.frame() diff --git a/checkers/variables.py b/checkers/variables.py index cdf6c22..73fdcc0 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -210,7 +210,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' except astroid.InferenceError: continue - if not isinstance(elt_name, astroid.Const) or not isinstance(elt_name.value, basestring): + if not isinstance(elt_name, astroid.Const) \ + or not isinstance(elt_name.value, basestring): self.add_message('E0604', args=elt.as_string(), node=elt) continue elt_name = elt_name.value @@ -230,8 +231,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' try: file_from_modpath(name.split(".")) except ImportError: - self.add_message('undefined-all-variable', - args=elt_name, + self.add_message('undefined-all-variable', + args=elt_name, node=elt) except SyntaxError, exc: # don't yield an syntax-error warning, @@ -547,10 +548,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' elif self._to_consume[-1][-1] != 'lambda': # E0601 may *not* occurs in lambda scope self.add_message('E0601', args=name, node=node) - if not isinstance(node, astroid.AssName): # Aug AssName - del to_consume[name] - else: + if isinstance(node, astroid.AssName): # Aug AssName del consumed[name] + else: + del to_consume[name] # check it's not a loop variable used outside the loop self._loopvar_name(node, name) break diff --git a/testutils.py b/testutils.py index 8d8064d..d60ab46 100644 --- a/testutils.py +++ b/testutils.py @@ -263,7 +263,8 @@ class LintTestUsingModule(testlib.TestCase): self._test(tocheck) def _check_result(self, got): - self.assertMultiLineEqual(self._get_expected().strip(), got.strip()) + self.assertMultiLineEqual(self._get_expected().strip()+'\n', + got.strip()+'\n') def _test(self, tocheck): if INFO_TEST_RGX.match(self.module): -- cgit v1.2.1 From 55c07618f5ec8f96c005477c5424449fa7700a4a Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 15 Apr 2014 18:51:46 +0200 Subject: [classes checker] fix access-member-before-definition false negative wrt aug assign. Closes #164 --- ChangeLog | 2 ++ checkers/classes.py | 17 +++++++++++++++++ test/unittest_checker_classes.py | 21 +++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 test/unittest_checker_classes.py diff --git a/ChangeLog b/ChangeLog index 30dbc07..dde3662 100644 --- a/ChangeLog +++ b/ChangeLog @@ -89,6 +89,8 @@ ChangeLog for Pylint * Fix unused-import false positive w/ augment assignment (#78) + * Fix access-member-before-definition false negative wrt aug assign (#164) + 2013-12-22 -- 1.1.0 * Add new check for use of deprecated pragma directives "pylint:disable-msg" diff --git a/checkers/classes.py b/checkers/classes.py index 6dd5df0..f5e2783 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -458,6 +458,10 @@ a metaclass class method.'} self._check_protected_attribute_access(node) + def visit_assattr(self, node): + if isinstance(node.ass_type(), astroid.AugAssign) and self.is_first_attr(node): + self._accessed[-1].setdefault(node.attrname, []).append(node) + @check_messages('protected-access') def visit_assign(self, assign_node): node = assign_node.targets[0] @@ -540,6 +544,19 @@ a metaclass class method.'} except astroid.NotFoundError: pass else: + # filter out augment assignment nodes + defstmts = [stmt for stmt in defstmts if stmt not in nodes] + if not defstmts: + # only augment assignment for this node, no-member should be + # triggered by the typecheck checker + continue + # filter defstmts to only pick the first one when there are + # several assignments in the same scope + scope = defstmts[0].scope() + defstmts = [stmt for i, stmt in enumerate(defstmts) + if i == 0 or stmt.scope() is not scope] + # if there are still more than one, don't attempt to be smarter + # than we can be if len(defstmts) == 1: defstmt = defstmts[0] # check that if the node is accessed in the same method as diff --git a/test/unittest_checker_classes.py b/test/unittest_checker_classes.py new file mode 100644 index 0000000..388a24a --- /dev/null +++ b/test/unittest_checker_classes.py @@ -0,0 +1,21 @@ +"""Unit tests for the variables checker.""" + +from astroid import test_utils +from pylint.checkers import classes +from pylint.testutils import CheckerTestCase, Message + +class VariablesCheckerTC(CheckerTestCase): + + CHECKER_CLASS = classes.ClassChecker + + def test_bitbucket_issue_164(self): + """Issue 164 report a false negative for access-member-before-definition""" + n1, n2 = test_utils.extract_node(""" + class MyClass1(object): + def __init__(self): + self.first += 5 #@ + self.first = 0 #@ + """) + with self.assertAddsMessages(Message('access-member-before-definition', + node=n1.target, args=('first', n2.lineno))): + self.walk(n1.root()) -- cgit v1.2.1 From b0b935b8f61d602baf56cfd0370a5a289311f2d3 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 16 Apr 2014 18:55:48 +0200 Subject: Do not attempt to analyze non python file, eg .so file. Closes #122 --- ChangeLog | 2 ++ lint.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index dde3662..68b980f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -91,6 +91,8 @@ ChangeLog for Pylint * Fix access-member-before-definition false negative wrt aug assign (#164) + * Do not attempt to analyze non python file, eg .so file (#122) + 2013-12-22 -- 1.1.0 * Add new check for use of deprecated pragma directives "pylint:disable-msg" diff --git a/lint.py b/lint.py index 3e2e18c..1be2fc9 100644 --- a/lint.py +++ b/lint.py @@ -584,8 +584,8 @@ warning, statement which respectively contain the number of errors / warnings\ def should_analyze_file(self, modname, path): # pylint: disable=unused-argument """Returns whether or not a module should be checked. - This implementation returns True for all inputs, indicating that all - files should be linted. + This implementation returns True for all python source file, indicating + that all files should be linted. Subclasses may override this method to indicate that modules satisfying certain conditions should not be linted. @@ -595,7 +595,7 @@ warning, statement which respectively contain the number of errors / warnings\ :returns: True if the module should be checked. :rtype: bool """ - return True + return path.endswith('.py') def check(self, files_or_modules): """main checking entry: check a list of files or modules from their -- cgit v1.2.1 From bf2520056a33522654fba7a605d88115d353e514 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Thu, 17 Apr 2014 17:44:23 +0200 Subject: remove optparse from default ignored-modules, we'll find another proper way to do that as needed --- checkers/typecheck.py | 2 +- test/test_type.py | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index ec44f8d..5fe293d 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -134,7 +134,7 @@ class should be ignored. A mixin class is detected if its name ends with \ "mixin" (case insensitive).'} ), ('ignored-modules', - {'default': ('optparse',), + {'default': (), 'type': 'csv', 'metavar': '', 'help': 'List of module names for which member attributes \ diff --git a/test/test_type.py b/test/test_type.py index 7c65215..352b6f9 100644 --- a/test/test_type.py +++ b/test/test_type.py @@ -23,28 +23,15 @@ class TypeCheckerTest(CheckerTestCase): args=('Module', 'argparse', 'THIS_does_not_EXIST'))): self.checker.visit_getattr(node) + @set_config(ignored_modules=('argparse',)) def test_no_member_in_getattr_ignored(self): """Make sure that a module attribute access check is omitted with a module that is configured to be ignored. """ - node = test_utils.extract_node(""" - import optparse - optparse.THIS_does_not_EXIST - """) - with self.assertNoMessages(): - self.checker.visit_getattr(node) - - - @set_config(ignored_modules=('argparse',)) - def test_no_member_in_getattr_ignored_cust_config(self): - """Make sure that a module can be added to the ignored_modules list and - a no-member message will not be raised upon attribute access. - """ - node = test_utils.extract_node(""" import argparse - argparse.THIS_does_not_EXIST + argparse.THIS_does_not_EXIST """) with self.assertNoMessages(): self.checker.visit_getattr(node) -- cgit v1.2.1 From 9ddebe38ecdeaecf435596f6dac22f9ca6c3e3c9 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Wed, 16 Apr 2014 22:23:36 +0200 Subject: Only emit symbolic warnings from the format checker. --- checkers/format.py | 34 +++++++++++++++++----------------- test/test_format.py | 46 +++++++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/checkers/format.py b/checkers/format.py index abe38a5..7ab6c7e 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -240,14 +240,14 @@ class FormatChecker(BaseTokenChecker): return if keyword_token == 'not': if not found_and_or: - self.add_message('C0325', line=line_num, + self.add_message('superfluous-parens', line=line_num, args=keyword_token) elif keyword_token in ('return', 'yield'): - self.add_message('C0325', line=line_num, + self.add_message('superfluous-parens', line=line_num, args=keyword_token) elif keyword_token not in self._keywords_with_parens: if not (tokens[i+1][1] == 'in' and found_and_or): - self.add_message('C0325', line=line_num, + self.add_message('superfluous-parens', line=line_num, args=keyword_token) return elif depth == 1: @@ -376,7 +376,7 @@ class FormatChecker(BaseTokenChecker): for policy, position in warnings: construct = _name_construct(tokens[i]) count, state = _policy_string(policy) - self.add_message('C0326', line=tokens[i][2][0], + self.add_message('bad-whitespace', line=tokens[i][2][0], args=(count, state, position, construct, _underline_token(tokens[i]))) @@ -437,7 +437,7 @@ class FormatChecker(BaseTokenChecker): self.new_line(tok_type, line, line_num, junk) if start[0] != line_num: if previous is not None and previous[0] == tokenize.OP and previous[1] == ';': - self.add_message('W0301', line=previous[2]) + self.add_message('unnecessary-semicolon', line=previous[2]) previous = None line_num = start[0] # A tokenizer oddity: if an indented line contains a multi-line @@ -453,10 +453,10 @@ class FormatChecker(BaseTokenChecker): if tok_type == tokenize.OP: if token == '<>': - self.add_message('W0331', line=line_num) + self.add_message('old-ne-operator', line=line_num) elif tok_type == tokenize.NUMBER: if token.endswith('l'): - self.add_message('W0332', line=line_num) + self.add_message('lowercase-l-suffix', line=line_num) elif tok_type == newline: # a program statement, or ENDMARKER, will eventually follow, @@ -499,9 +499,9 @@ class FormatChecker(BaseTokenChecker): line_num -= 1 # to be ok with "wc -l" if line_num > self.config.max_module_lines: - self.add_message('C0302', args=line_num, line=1) + self.add_message('too-many-lines', args=line_num, line=1) - @check_messages('C0321', 'C03232', 'C0323', 'C0324') + @check_messages('multiple-statements') def visit_default(self, node): """check the node line number and check it if not yet done""" if not node.is_statement: @@ -555,12 +555,12 @@ class FormatChecker(BaseTokenChecker): if (isinstance(node.parent, nodes.If) and not node.parent.orelse and self.config.single_line_if_stmt): return - self.add_message('C0321', node=node) + self.add_message('multiple-statements', node=node) self._visited_lines[line] = 2 - @check_messages('W0333') + @check_messages('backtick') def visit_backquote(self, node): - self.add_message('W0333', node=node) + self.add_message('backtick', node=node) def check_lines(self, lines, i): """check lines have less than a maximum number of characters @@ -570,11 +570,11 @@ class FormatChecker(BaseTokenChecker): for line in lines.splitlines(True): if not line.endswith('\n'): - self.add_message('C0304', line=i) + self.add_message('missing-final-newline', line=i) else: stripped_line = line.rstrip() if line[len(stripped_line):] not in ('\n', '\r\n'): - self.add_message('C0303', line=i) + self.add_message('trailing-whitespace', line=i) # Don't count excess whitespace in the line length. line = stripped_line mobj = OPTION_RGX.search(line) @@ -582,7 +582,7 @@ class FormatChecker(BaseTokenChecker): line = line.split('#')[0].rstrip() if len(line) > max_chars and not ignore_long_line.search(line): - self.add_message('C0301', line=i, args=(len(line), max_chars)) + self.add_message('line-too-long', line=i, args=(len(line), max_chars)) i += 1 def check_indent_level(self, string, expected, line_num): @@ -603,7 +603,7 @@ class FormatChecker(BaseTokenChecker): args = ('tab', 'space') else: args = ('space', 'tab') - self.add_message('W0312', args=args, line=line_num) + self.add_message('mixed-indentation', args=args, line=line_num) return level suppl += string[0] string = string[1:] @@ -611,7 +611,7 @@ class FormatChecker(BaseTokenChecker): i_type = 'spaces' if indent[0] == '\t': i_type = 'tabs' - self.add_message('W0311', line=line_num, + self.add_message('bad-indentation', line=line_num, args=(level * unit_size + len(suppl), i_type, expected * unit_size)) diff --git a/test/test_format.py b/test/test_format.py index 9494f75..8f1be78 100644 --- a/test/test_format.py +++ b/test/test_format.py @@ -41,7 +41,7 @@ class MultiStatementLineTest(CheckerTestCase): stmt = test_utils.extract_node(""" if True: pass #@ """) - with self.assertAddsMessages(Message('C0321', node=stmt.body[0])): + with self.assertAddsMessages(Message('multiple-statements', node=stmt.body[0])): self.checker.process_tokens([]) self.checker.visit_default(stmt.body[0]) self.checker.config.single_line_if_stmt = True @@ -53,7 +53,7 @@ class MultiStatementLineTest(CheckerTestCase): else: pass """) - with self.assertAddsMessages(Message('C0321', node=stmt.body[0])): + with self.assertAddsMessages(Message('multiple-statements', node=stmt.body[0])): self.checker.process_tokens([]) self.checker.visit_default(stmt.body[0]) @@ -97,21 +97,21 @@ class SuperfluousParenthesesTest(CheckerTestCase): def testCheckKeywordParensHandlesUnnecessaryParens(self): self.checker._keywords_with_parens = set() cases = [ - (Message('C0325', line=1, args='if'), + (Message('superfluous-parens', line=1, args='if'), 'if (foo):', 0), - (Message('C0325', line=1, args='if'), + (Message('superfluous-parens', line=1, args='if'), 'if ((foo, bar)):', 0), - (Message('C0325', line=1, args='if'), + (Message('superfluous-parens', line=1, args='if'), 'if (foo(bar)):', 0), - (Message('C0325', line=1, args='return'), + (Message('superfluous-parens', line=1, args='return'), 'return ((x for x in x))', 0), - (Message('C0325', line=1, args='not'), + (Message('superfluous-parens', line=1, args='not'), 'not (foo)', 0), - (Message('C0325', line=1, args='not'), + (Message('superfluous-parens', line=1, args='not'), 'if not (foo):', 1), - (Message('C0325', line=1, args='if'), + (Message('superfluous-parens', line=1, args='if'), 'if (not (foo)):', 0), - (Message('C0325', line=1, args='not'), + (Message('superfluous-parens', line=1, args='not'), 'if (not (foo)):', 2), ] for msg, code, offset in cases: @@ -143,22 +143,22 @@ class CheckSpaceTest(CheckerTestCase): def testParenthesesBad(self): with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'after', 'bracket', '( a)\n^'))): self.checker.process_tokens(tokenize_str('( a)\n')) with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'before', 'bracket', '(a )\n ^'))): self.checker.process_tokens(tokenize_str('(a )\n')) with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'before', 'bracket', 'foo (a)\n ^'))): self.checker.process_tokens(tokenize_str('foo (a)\n')) with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'before', 'bracket', '{1: 2} [1]\n ^'))): self.checker.process_tokens(tokenize_str('{1: 2} [1]\n')) @@ -174,13 +174,13 @@ class CheckSpaceTest(CheckerTestCase): @set_config(no_space_check=[]) def testTrailingCommaBad(self): with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'before', 'bracket', '(a, )\n ^'))): self.checker.process_tokens(tokenize_str('(a, )\n')) def testComma(self): with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'before', 'comma', '(a , b)\n ^'))): self.checker.process_tokens(tokenize_str('(a , b)\n')) @@ -204,19 +204,19 @@ class CheckSpaceTest(CheckerTestCase): def testKeywordSpacingBad(self): with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'before', 'keyword argument assignment', '(foo =bar)\n ^'))): self.checker.process_tokens(tokenize_str('(foo =bar)\n')) with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'after', 'keyword argument assignment', '(foo= bar)\n ^'))): self.checker.process_tokens(tokenize_str('(foo= bar)\n')) with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('No', 'allowed', 'around', 'keyword argument assignment', '(foo = bar)\n ^'))): self.checker.process_tokens(tokenize_str('(foo = bar)\n')) @@ -233,22 +233,22 @@ class CheckSpaceTest(CheckerTestCase): def testOperatorSpacingBad(self): with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('Exactly one', 'required', 'before', 'comparison', 'a< b\n ^'))): self.checker.process_tokens(tokenize_str('a< b\n')) with self.assertAddsMessages( - Message('C0326', line=1, + Message('bad-whitespace', line=1, args=('Exactly one', 'required', 'after', 'comparison', 'a Date: Wed, 16 Apr 2014 22:36:13 +0200 Subject: Only emit symblic warnings from the variables checker. --- checkers/variables.py | 61 +++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/checkers/variables.py b/checkers/variables.py index 73fdcc0..d74cb56 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -192,9 +192,9 @@ builtins. Remember that you should avoid to define new builtins when possible.' for name, stmts in node.locals.iteritems(): if is_builtin(name) and not is_inside_except(stmts[0]): # do not print Redefining builtin for additional builtins - self.add_message('W0622', args=name, node=stmts[0]) + self.add_message('redefined-builtin', args=name, node=stmts[0]) - @check_messages('W0611', 'W0614', 'W0622', 'E0603', 'E0604') + @check_messages('unused-import', 'unused-wildcard-import', 'redefined-builtin', 'undefined-all-variable', 'invalid-all-object') def leave_module(self, node): """leave module: check globals """ @@ -212,7 +212,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' if not isinstance(elt_name, astroid.Const) \ or not isinstance(elt_name.value, basestring): - self.add_message('E0604', args=elt.as_string(), node=elt) + self.add_message('invalid-all-object', args=elt.as_string(), node=elt) continue elt_name = elt_name.value # If elt is in not_consumed, remove it from not_consumed @@ -249,12 +249,12 @@ builtins. Remember that you should avoid to define new builtins when possible.' continue stmt = stmts[0] if isinstance(stmt, astroid.Import): - self.add_message('W0611', args=name, node=stmt) + self.add_message('unused-import', args=name, node=stmt) elif isinstance(stmt, astroid.From) and stmt.modname != '__future__': if stmt.names[0][0] == '*': - self.add_message('W0614', args=name, node=stmt) + self.add_message('unused-wildcard-import', args=name, node=stmt) else: - self.add_message('W0611', args=name, node=stmt) + self.add_message('unused-import', args=name, node=stmt) del self._to_consume def visit_class(self, node): @@ -316,7 +316,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' """visit function: update consumption analysis variable and check locals """ self._to_consume.append((copy(node.locals), {}, 'function')) - if not set(('W0621', 'W0622')) & self.active_msgs: + if not (self.linter.is_message_enabled('redefined-outer-name') or + self.linter.is_message_enabled('redefined-builtin')): return globs = node.root().globals for name, stmt in node.items(): @@ -326,15 +327,16 @@ builtins. Remember that you should avoid to define new builtins when possible.' line = globs[name][0].fromlineno dummy_rgx = self.config.dummy_variables_rgx if not dummy_rgx.match(name): - self.add_message('W0621', args=(name, line), node=stmt) + self.add_message('redefined-outer-name', args=(name, line), node=stmt) elif is_builtin(name): # do not print Redefining builtin for additional builtins - self.add_message('W0622', args=name, node=stmt) + self.add_message('redefined-builtin', args=name, node=stmt) def leave_function(self, node): """leave function: check function's locals are consumed""" not_consumed = self._to_consume.pop()[0] - if not set(('W0612', 'W0613')) & self.active_msgs: + if not (self.linter.is_message_enabled('unused-variable') or + self.linter.is_message_enabled('unused-argument')): return # don't check arguments of function which are only raising an exception if is_error(node): @@ -373,16 +375,17 @@ builtins. Remember that you should avoid to define new builtins when possible.' # don't check callback arguments XXX should be configurable if node.name.startswith('cb_') or node.name.endswith('_cb'): continue - self.add_message('W0613', args=name, node=stmt) + self.add_message('unused-argument', args=name, node=stmt) else: - self.add_message('W0612', args=name, node=stmt) + self.add_message('unused-variable', args=name, node=stmt) - @check_messages('W0601', 'W0602', 'W0603', 'W0604', 'W0622') + @check_messages('global-variable-undefined', 'global-variable-not-assigned', 'global-statement', + 'global-at-module-level', 'redefined-builtin') def visit_global(self, node): """check names imported exists in the global scope""" frame = node.frame() if isinstance(frame, astroid.Module): - self.add_message('W0604', node=node) + self.add_message('global-at-module-level', node=node) return module = frame.root() default_message = True @@ -402,23 +405,23 @@ builtins. Remember that you should avoid to define new builtins when possible.' break else: # global but no assignment - self.add_message('W0602', args=name, node=node) + self.add_message('global-variable-not-assigned', args=name, node=node) default_message = False if not assign_nodes: continue for anode in assign_nodes: if anode.parent is None: - self.add_message('W0622', args=name, node=node) + self.add_message('redefined-builtin', args=name, node=node) break if anode.frame() is module: # module level assignment break else: # global undefined at the module scope - self.add_message('W0601', args=name, node=node) + self.add_message('global-variable-undefined', args=name, node=node) default_message = False if default_message: - self.add_message('W0603', node=node) + self.add_message('global-statement', node=node) def _loopvar_name(self, node, name): # filter variables according to node's scope @@ -428,7 +431,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' #astmts = [stmt for stmt in node.lookup(name)[1] # if hasattr(stmt, 'ass_type')] and # not stmt.statement().parent_of(node)] - if 'W0631' not in self.active_msgs: + if not self.linter.is_message_enabled('undefined-loop-variable'): return astmts = [stmt for stmt in node.lookup(name)[1] if hasattr(stmt, 'ass_type')] @@ -454,14 +457,14 @@ builtins. Remember that you should avoid to define new builtins when possible.' ass = astmts[0].ass_type() if isinstance(ass, (astroid.For, astroid.Comprehension, astroid.GenExpr)) \ and not ass.statement() is node.statement(): - self.add_message('W0631', args=name, node=node) + self.add_message('undefined-loop-variable', args=name, node=node) - @check_messages('W0623') + @check_messages('redefine-in-handler') def visit_excepthandler(self, node): for name in get_all_elements(node.name): clobbering, args = clobber_in_except(name) if clobbering: - self.add_message('W0623', args=args, node=name) + self.add_message('redefine-in-handler', args=args, node=name) def visit_assname(self, node): if isinstance(node.ass_type(), astroid.AugAssign): @@ -544,10 +547,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' and not are_exclusive(stmt, defstmt, ('NameError', 'Exception', 'BaseException'))): if defstmt is stmt and isinstance(node, (astroid.DelName, astroid.AssName)): - self.add_message('E0602', args=name, node=node) + self.add_message('undefined-variable', args=name, node=node) elif self._to_consume[-1][-1] != 'lambda': # E0601 may *not* occurs in lambda scope - self.add_message('E0601', args=name, node=node) + self.add_message('used-before-assignment', args=name, node=node) if isinstance(node, astroid.AssName): # Aug AssName del consumed[name] else: @@ -560,9 +563,9 @@ builtins. Remember that you should avoid to define new builtins when possible.' # undefined name ! if not (name in astroid.Module.scope_attrs or is_builtin(name) or name in self.config.additional_builtins): - self.add_message('E0602', args=name, node=node) + self.add_message('undefined-variable', args=name, node=node) - @check_messages('E0611') + @check_messages('no-name-in-module') def visit_import(self, node): """check modules attribute accesses""" for name, _ in node.names: @@ -573,7 +576,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' continue self._check_module_attrs(node, module, parts[1:]) - @check_messages('E0611') + @check_messages('no-name-in-module') def visit_from(self, node): """check modules attribute accesses""" name_parts = node.modname.split('.') @@ -658,7 +661,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' if module is astroid.YES: return None except astroid.NotFoundError: - self.add_message('E0611', args=(name, module.name), node=node) + self.add_message('no-name-in-module', args=(name, module.name), node=node) return None except astroid.InferenceError: return None @@ -666,7 +669,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' # FIXME: other message if name is not the latest part of # module_names ? modname = module and module.name or '__dict__' - self.add_message('E0611', node=node, + self.add_message('no-name-in-module', node=node, args=('.'.join(module_names), modname)) return None if isinstance(module, astroid.Module): -- cgit v1.2.1 From 08825bfe8d5e572dd0b4a9b72628cc102b5ee5a1 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Wed, 16 Apr 2014 22:39:12 +0200 Subject: Stop setting active_msgs on checkers, as they can just rely on is_message_enabled on the linter. --- checkers/__init__.py | 2 -- lint.py | 1 - 2 files changed, 3 deletions(-) diff --git a/checkers/__init__.py b/checkers/__init__.py index 9346904..af7965b 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -91,8 +91,6 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): self.name = self.name.lower() OptionsProviderMixIn.__init__(self) self.linter = linter - # messages that are active for the current check - self.active_msgs = set() def add_message(self, msg_id, line=None, node=None, args=None): """add a message of a given type""" diff --git a/lint.py b/lint.py index 1be2fc9..5bafdc1 100644 --- a/lint.py +++ b/lint.py @@ -578,7 +578,6 @@ warning, statement which respectively contain the number of errors / warnings\ if (messages or any(self.report_is_enabled(r[0]) for r in checker.reports)): neededcheckers.append(checker) - checker.active_msgs = messages return neededcheckers def should_analyze_file(self, modname, path): # pylint: disable=unused-argument -- cgit v1.2.1 From 851fd3a32ecbf5c66a6bbd98c9f9103f24baa483 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 11:01:23 +0200 Subject: Only emit symbolic warnings from the design analysis checker. --- checkers/design_analysis.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/checkers/design_analysis.py b/checkers/design_analysis.py index cfd2d80..c9ef4df 100644 --- a/checkers/design_analysis.py +++ b/checkers/design_analysis.py @@ -185,15 +185,18 @@ class MisdesignChecker(BaseChecker): """check that abstract/interface classes are used""" for abstract in self._abstracts: if not abstract in self._used_abstracts: - self.add_message('R0921', node=abstract) + self.add_message('abstract-class-not-used', node=abstract) elif self._used_abstracts[abstract] < 2: - self.add_message('R0922', node=abstract, + self.add_message('abstract-class-little-used', node=abstract, args=self._used_abstracts[abstract]) for iface in self._ifaces: if not iface in self._used_ifaces: - self.add_message('R0923', node=iface) + self.add_message('interface-not-implemented', node=iface) - @check_messages('R0901', 'R0902', 'R0903', 'R0904', 'R0921', 'R0922', 'R0923') + @check_messages('too-many-ancestors', 'too-many-instance-attributes', + 'too-few-public-methods', 'too-many-public-methods', + 'abstract-class-not-used', 'abstract-class-little-used', + 'interface-not-implemented') def visit_class(self, node): """check size of inheritance hierarchy and number of instance attributes """ @@ -201,13 +204,13 @@ class MisdesignChecker(BaseChecker): # Is the total inheritance hierarchy is 7 or less? nb_parents = len(list(node.ancestors())) if nb_parents > self.config.max_parents: - self.add_message('R0901', node=node, + self.add_message('too-many-ancestors', node=node, args=(nb_parents, self.config.max_parents)) # Does the class contain less than 20 attributes for # non-GUI classes (40 for GUI)? # FIXME detect gui classes if len(node.instance_attrs) > self.config.max_attributes: - self.add_message('R0902', node=node, + self.add_message('too-many-instance-attributes', node=node, args=(len(node.instance_attrs), self.config.max_attributes)) # update abstract / interface classes structures @@ -231,7 +234,10 @@ class MisdesignChecker(BaseChecker): except KeyError: self._used_abstracts[parent] = 1 - @check_messages('R0901', 'R0902', 'R0903', 'R0904', 'R0921', 'R0922', 'R0923') + @check_messages('too-many-ancestors', 'too-many-instance-attributes', + 'too-few-public-methods', 'too-many-public-methods', + 'abstract-class-not-used', 'abstract-class-little-used', + 'interface-not-implemented') def leave_class(self, node): """check number of public methods""" nb_public_methods = 0 @@ -243,7 +249,7 @@ class MisdesignChecker(BaseChecker): special_methods.add(method.name) # Does the class contain less than 20 public methods ? if nb_public_methods > self.config.max_public_methods: - self.add_message('R0904', node=node, + self.add_message('too-many-public-methods', node=node, args=(nb_public_methods, self.config.max_public_methods)) # stop here for exception, metaclass and interface classes @@ -255,7 +261,8 @@ class MisdesignChecker(BaseChecker): args=(nb_public_methods, self.config.min_public_methods)) - @check_messages('R0911', 'R0912', 'R0913', 'R0914', 'R0915') + @check_messages('too-many-return-statements', 'too-many-branches', + 'too-many-arguments', 'too-many-locals', 'too-many-statements') def visit_function(self, node): """check function name, docstring, arguments, redefinition, variable names, max locals @@ -272,34 +279,34 @@ class MisdesignChecker(BaseChecker): if self.config.ignored_argument_names.match(arg.name)]) argnum = len(args) - ignored_args_num if argnum > self.config.max_args: - self.add_message('R0913', node=node, + self.add_message('too-many-arguments', node=node, args=(len(args), self.config.max_args)) else: ignored_args_num = 0 # check number of local variables locnum = len(node.locals) - ignored_args_num if locnum > self.config.max_locals: - self.add_message('R0914', node=node, + self.add_message('too-many-locals', node=node, args=(locnum, self.config.max_locals)) # init statements counter self._stmts = 1 - @check_messages('R0911', 'R0912', 'R0913', 'R0914', 'R0915') + @check_messages('too-many-return-statements', 'too-many-branches', 'too-many-arguments', 'too-many-locals', 'too-many-statements') def leave_function(self, node): """most of the work is done here on close: checks for max returns, branch, return in __init__ """ returns = self._returns.pop() if returns > self.config.max_returns: - self.add_message('R0911', node=node, + self.add_message('too-many-return-statements', node=node, args=(returns, self.config.max_returns)) branches = self._branches.pop() if branches > self.config.max_branches: - self.add_message('R0912', node=node, + self.add_message('too-many-branches', node=node, args=(branches, self.config.max_branches)) # check number of statements if self._stmts > self.config.max_statements: - self.add_message('R0915', node=node, + self.add_message('too-many-statements', node=node, args=(self._stmts, self.config.max_statements)) def visit_return(self, _): -- cgit v1.2.1 From 165f17bdb491dfb62eff8edc1956c0590758c33b Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 11:04:41 +0200 Subject: Only emit symbolic warnings from the logging checker. --- checkers/logging.py | 10 +++++----- test/test_logging.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/checkers/logging.py b/checkers/logging.py index dbd1e7b..cbdf0f2 100644 --- a/checkers/logging.py +++ b/checkers/logging.py @@ -149,7 +149,7 @@ class LoggingChecker(checkers.BaseChecker): return if isinstance(node.args[format_pos], astroid.BinOp) and node.args[format_pos].op == '%': - self.add_message('W1201', node=node) + self.add_message('logging-not-lazy', node=node) elif isinstance(node.args[format_pos], astroid.Const): self._check_format_string(node, format_pos) @@ -180,16 +180,16 @@ class LoggingChecker(checkers.BaseChecker): return except utils.UnsupportedFormatCharacter, ex: char = format_string[ex.index] - self.add_message('E1200', node=node, + self.add_message('logging-unsupported-format', node=node, args=(char, ord(char), ex.index)) return except utils.IncompleteFormatString: - self.add_message('E1201', node=node) + self.add_message('logging-format-truncated', node=node) return if num_args > required_num_args: - self.add_message('E1205', node=node) + self.add_message('logging-too-many-args', node=node) elif num_args < required_num_args: - self.add_message('E1206', node=node) + self.add_message('logging-too-few-args', node=node) def _count_supplied_tokens(args): diff --git a/test/test_logging.py b/test/test_logging.py index fe7e638..cbacada 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -20,7 +20,7 @@ class LoggingModuleDetectionTest(CheckerTestCase): """) self.checker.visit_module(None) self.checker.visit_import(stmts[0]) - with self.assertAddsMessages(Message('W1201', node=stmts[1])): + with self.assertAddsMessages(Message('logging-not-lazy', node=stmts[1])): self.checker.visit_callfunc(stmts[1]) def test_detects_renamed_standard_logging_module(self): @@ -30,7 +30,7 @@ class LoggingModuleDetectionTest(CheckerTestCase): """) self.checker.visit_module(None) self.checker.visit_import(stmts[0]) - with self.assertAddsMessages(Message('W1201', node=stmts[1])): + with self.assertAddsMessages(Message('logging-not-lazy', node=stmts[1])): self.checker.visit_callfunc(stmts[1]) @set_config(logging_modules=['logging', 'my.logging']) @@ -41,7 +41,7 @@ class LoggingModuleDetectionTest(CheckerTestCase): """) self.checker.visit_module(None) self.checker.visit_import(stmts[0]) - with self.assertAddsMessages(Message('W1201', node=stmts[1])): + with self.assertAddsMessages(Message('logging-not-lazy', node=stmts[1])): self.checker.visit_callfunc(stmts[1]) -- cgit v1.2.1 From 55cee05bc4ad266e314610e35cf41eab55377e07 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 11:07:03 +0200 Subject: Only emit symbolic warnings from the misc checker. --- checkers/misc.py | 4 ++-- test/test_misc.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/checkers/misc.py b/checkers/misc.py index 9c49825..d1b7c21 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -57,13 +57,13 @@ separated by a comma.' def _check_note(self, notes, lineno, line): match = notes.search(line) if match: - self.add_message('W0511', args=line[match.start():-1], line=lineno) + self.add_message('fixme', args=line[match.start():-1], line=lineno) def _check_encoding(self, lineno, line, file_encoding): try: return unicode(line, file_encoding) except UnicodeDecodeError, ex: - self.add_message('W0512', line=lineno, + self.add_message('invalid-encoded-data', line=lineno, args=(file_encoding, ex.args[2])) def process_module(self, module): diff --git a/test/test_misc.py b/test/test_misc.py index 5a10936..5a9ccd6 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -55,7 +55,7 @@ class FixmeTest(CheckerTestCase): """a = 1 # FIXME """) as module: with self.assertAddsMessages( - Message(msg_id='W0511', line=2, args=u'FIXME')): + Message(msg_id='fixme', line=2, args=u'FIXME')): self.checker.process_module(module) @set_config(notes=[]) -- cgit v1.2.1 From 470132dbf57f63bc7c71dea1d7cdd47076f245d8 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 11:17:44 +0200 Subject: Only use and emit symbolic warnings in the main linter module --- lint.py | 40 ++++++++++++++++++++-------------------- test/messages/func_i0011.txt | 2 +- test/messages/func_i0012.txt | 3 +-- test/messages/func_i0020.txt | 2 +- test/messages/func_i0022.txt | 2 +- utils.py | 11 ++++++----- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lint.py b/lint.py index 5bafdc1..0cda818 100644 --- a/lint.py +++ b/lint.py @@ -109,11 +109,11 @@ MSGS = { 'Used when an inline option is either badly formatted or can\'t ' 'be used inside modules.'), - 'I0011': ('Locally disabling %s', + 'I0011': ('Locally disabling %s (%s)', 'locally-disabled', 'Used when an inline option disables a message or a messages ' 'category.'), - 'I0012': ('Locally enabling %s', + 'I0012': ('Locally enabling %s (%s)', 'locally-enabled', 'Used when an inline option enables a message or a messages ' 'category.'), @@ -473,14 +473,14 @@ warning, statement which respectively contain the number of errors / warnings\ if match.group(1).strip() == "disable-all" or \ match.group(1).strip() == 'skip-file': if match.group(1).strip() == "disable-all": - self.add_message('I0014', line=start[0]) - self.add_message('I0013', line=start[0]) + self.add_message('deprecated-disable-all', line=start[0]) + self.add_message('file-ignored', line=start[0]) self._ignore_file = True return try: opt, value = match.group(1).split('=', 1) except ValueError: - self.add_message('I0010', args=match.group(1).strip(), + self.add_message('bad-inline-option', args=match.group(1).strip(), line=start[0]) continue opt = opt.strip() @@ -494,15 +494,15 @@ warning, statement which respectively contain the number of errors / warnings\ for msgid in splitstrip(value): try: if (opt, msgid) == ('disable', 'all'): - self.add_message('I0014', line=start[0]) - self.add_message('I0013', line=start[0]) + self.add_message('deprecated-disable-all', line=start[0]) + self.add_message('file-ignored', line=start[0]) self._ignore_file = True return meth(msgid, 'module', start[0]) except UnknownMessage: - self.add_message('E0012', args=msgid, line=start[0]) + self.add_message('bad-option-value', args=msgid, line=start[0]) else: - self.add_message('E0011', args=opt, line=start[0]) + self.add_message('unrecognized-inline-option', args=opt, line=start[0]) def collect_block_lines(self, node, msg_state): """walk ast to collect block level options line numbers""" @@ -648,7 +648,7 @@ warning, statement which respectively contain the number of errors / warnings\ message = modname = error["mod"] key = error["key"] self.set_current_module(modname) - if key == "F0001": + if key == "fatal": message = str(error["ex"]).replace(os.getcwd() + os.sep, '') self.add_message(key, args=message) return result @@ -679,13 +679,13 @@ warning, statement which respectively contain the number of errors / warnings\ try: return MANAGER.ast_from_file(filepath, modname, source=True) except SyntaxError, ex: - self.add_message('E0001', line=ex.lineno, args=ex.msg) + self.add_message('syntax-error', line=ex.lineno, args=ex.msg) except AstroidBuildingException, ex: - self.add_message('F0010', args=ex) + self.add_message('parse-error', args=ex) except Exception, ex: import traceback traceback.print_exc() - self.add_message('F0002', args=(ex.__class__, ex)) + self.add_message('astroid-error', args=(ex.__class__, ex)) def check_astroid_module(self, astroid, walker, rawcheckers, tokencheckers): """check a module from its astroid representation, real work""" @@ -693,11 +693,11 @@ warning, statement which respectively contain the number of errors / warnings\ try: tokens = tokenize_module(astroid) except tokenize.TokenError, ex: - self.add_message('E0001', line=ex.args[1][0], args=ex.args[0]) + self.add_message('syntax-error', line=ex.args[1][0], args=ex.args[0]) return if not astroid.pure_python: - self.add_message('I0001', args=astroid.name) + self.add_message('raw-checker-failed', args=astroid.name) else: #assert astroid.file.endswith('.py') # invoke ITokenChecker interface on self to fetch module/block @@ -761,12 +761,12 @@ warning, statement which respectively contain the number of errors / warnings\ for warning, lines in self._raw_module_msgs_state.iteritems(): for line, enable in lines.iteritems(): if not enable and (warning, line) not in self._ignored_msgs: - self.add_message('I0021', line, None, + self.add_message('useless-suppression', line, None, (self.get_msg_display_string(warning),)) # don't use iteritems here, _ignored_msgs may be modified by add_message for (warning, from_), lines in self._ignored_msgs.items(): for line in lines: - self.add_message('I0020', line, None, + self.add_message('suppressed-message', line, None, (self.get_msg_display_string(warning), from_)) def report_evaluation(self, sect, stats, previous_stats): @@ -1013,9 +1013,9 @@ are done by default'''}), 'been issued by analysing pylint output status code\n', level=1) # read configuration - linter.disable('W0704') - linter.disable('I0020') - linter.disable('I0021') + linter.disable('pointless-except') + linter.disable('suppressed-message') + linter.disable('useless-suppression') linter.read_config_file() # is there some additional plugins in the file configuration, in config_parser = linter.cfgfile_parser diff --git a/test/messages/func_i0011.txt b/test/messages/func_i0011.txt index 87c1f1f..6c1071d 100644 --- a/test/messages/func_i0011.txt +++ b/test/messages/func_i0011.txt @@ -1,2 +1,2 @@ -I: 1: Locally disabling W0404 +I: 1: Locally disabling reimported (W0404) I: 1: Useless suppression of 'reimported' diff --git a/test/messages/func_i0012.txt b/test/messages/func_i0012.txt index bf0c90f..76b0c77 100644 --- a/test/messages/func_i0012.txt +++ b/test/messages/func_i0012.txt @@ -1,2 +1 @@ -I: 1: Locally enabling W0404 - +I: 1: Locally enabling reimported (W0404) diff --git a/test/messages/func_i0020.txt b/test/messages/func_i0020.txt index e25be4e..73f83a4 100644 --- a/test/messages/func_i0020.txt +++ b/test/messages/func_i0020.txt @@ -1,2 +1,2 @@ -I: 7: Locally disabling W0612 +I: 7: Locally disabling unused-variable (W0612) I: 8: Suppressed 'unused-variable' (from line 7) diff --git a/test/messages/func_i0022.txt b/test/messages/func_i0022.txt index c882cae..471b8e3 100644 --- a/test/messages/func_i0022.txt +++ b/test/messages/func_i0022.txt @@ -1,4 +1,4 @@ -I: 5: Locally disabling C0103 +I: 5: Locally disabling invalid-name (C0103) I: 5: Suppressed 'invalid-name' (from line 5) I: 6: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" I: 6: Suppressed 'invalid-name' (from line 6) diff --git a/utils.py b/utils.py index 804229a..aee119e 100644 --- a/utils.py +++ b/utils.py @@ -286,8 +286,9 @@ class MessagesHandlerMixIn(object): self._module_msgs_state[msg.msgid][line] = False except KeyError: self._module_msgs_state[msg.msgid] = {line: False} - if msgid != 'I0011': - self.add_message('I0011', line=line, args=msg.msgid) + if msg.symbol != 'locally-disabled': + self.add_message('locally-disabled', line=line, + args=(msg.symbol, msg.msgid)) else: msgs = self._msgs_state @@ -330,7 +331,7 @@ class MessagesHandlerMixIn(object): self._module_msgs_state[msg.msgid][line] = True except KeyError: self._module_msgs_state[msg.msgid] = {line: True} - self.add_message('I0012', line=line, args=msg.msgid) + self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid)) else: msgs = self._msgs_state msgs[msg.msgid] = True @@ -629,12 +630,12 @@ def expand_modules(files_or_modules, black_list): try: filepath = file_from_modpath(modname.split('.')) if filepath is None: - errors.append({'key' : 'F0003', 'mod': modname}) + errors.append({'key' : 'ignored-builtin-module', 'mod': modname}) continue except (ImportError, SyntaxError), ex: # FIXME p3k : the SyntaxError is a Python bug and should be # removed as soon as possible http://bugs.python.org/issue10588 - errors.append({'key': 'F0001', 'mod': modname, 'ex': ex}) + errors.append({'key': 'fatal', 'mod': modname, 'ex': ex}) continue filepath = normpath(filepath) result.append({'path': filepath, 'name': modname, -- cgit v1.2.1 From 39c007ffd6e7eceee949ccecae8cb91abf28a029 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 11:21:10 +0200 Subject: Remove F0004[unexpected-inferred-value], it's not used anywhere. --- lint.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lint.py b/lint.py index 0cda818..ebf3a1f 100644 --- a/lint.py +++ b/lint.py @@ -90,10 +90,6 @@ MSGS = { 'ignored-builtin-module', 'Used to indicate that the user asked to analyze a builtin ' 'module which has been skipped.'), - 'F0004': ('unexpected inferred value %s', - 'unexpected-inferred-value', - 'Used to indicate that some value of an unexpected type has been ' - 'inferred.'), 'F0010': ('error while code parsing: %s', 'parse-error', 'Used when an exception occured while building the Astroid ' -- cgit v1.2.1 From ccd6318a50e7428dfeb7da74f19a332b71ba5c9c Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 11:41:32 +0200 Subject: Use one symbol for warning about deprecated pragmas. --- lint.py | 22 +++++++++------------- test/messages/func_i0014.txt | 2 +- test/messages/func_i0022.txt | 10 +++++----- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lint.py b/lint.py index ebf3a1f..49eea73 100644 --- a/lint.py +++ b/lint.py @@ -116,12 +116,6 @@ MSGS = { 'I0013': ('Ignoring entire file', 'file-ignored', 'Used to inform that the file will not be checked'), - 'I0014': ('Used deprecated directive "pylint:disable-all" or ' - '"pylint:disable=all"', - 'deprecated-disable-all', - 'You should preferably use "pylint:skip-file" as this directive ' - 'has a less confusing name. Do this only if you are sure that ' - 'all people running Pylint on your code have version >= 0.26'), 'I0020': ('Suppressed %s (from line %d)', 'suppressed-message', 'A message was triggered on a line, but suppressed explicitly ' @@ -132,11 +126,12 @@ MSGS = { 'useless-suppression', 'Reported when a message is explicitly disabled for a line or ' 'a block of code, but never triggered.'), - 'I0022': ('Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg"', + 'I0022': ('Pragma "%s" is deprecated, use "%s" instead', 'deprecated-pragma', - 'You should preferably use "pylint:disable" or "pylint:enable" ' - 'instead of the deprecated suppression pragma style ' - '"pylint:disable-msg" or "pylint:enable-msg"'), + 'Some inline pylint options have been renamed or reworked, ' + 'only the most recent form should be used. ' + 'NOTE:skip-all is only available with pylint >= 0.26', + {'old_names': [('I0014', 'deprecated-disable-all')]}), 'E0001': ('%s', 'syntax-error', @@ -469,7 +464,8 @@ warning, statement which respectively contain the number of errors / warnings\ if match.group(1).strip() == "disable-all" or \ match.group(1).strip() == 'skip-file': if match.group(1).strip() == "disable-all": - self.add_message('deprecated-disable-all', line=start[0]) + self.add_message('deprecated-pragma', line=start[0], + args=('disable-all', 'skip-file')) self.add_message('file-ignored', line=start[0]) self._ignore_file = True return @@ -486,11 +482,11 @@ warning, statement which respectively contain the number of errors / warnings\ except KeyError: meth = self._bw_options_methods[opt] # found a "(dis|en)able-msg" pragma deprecated suppresssion - self.add_message('deprecated-pragma', line=start[0]) + self.add_message('deprecated-pragma', line=start[0], args=(opt, opt.replace('-msg', ''))) for msgid in splitstrip(value): try: if (opt, msgid) == ('disable', 'all'): - self.add_message('deprecated-disable-all', line=start[0]) + self.add_message('deprecated-pragma', line=start[0], args=('disable=all', 'skip-file')) self.add_message('file-ignored', line=start[0]) self._ignore_file = True return diff --git a/test/messages/func_i0014.txt b/test/messages/func_i0014.txt index d179cd1..c3b521d 100644 --- a/test/messages/func_i0014.txt +++ b/test/messages/func_i0014.txt @@ -1,2 +1,2 @@ I: 1: Ignoring entire file -I: 1: Used deprecated directive "pylint:disable-all" or "pylint:disable=all" +I: 1: Pragma "disable-all" is deprecated, use "skip-file" instead diff --git a/test/messages/func_i0022.txt b/test/messages/func_i0022.txt index 471b8e3..ddcb066 100644 --- a/test/messages/func_i0022.txt +++ b/test/messages/func_i0022.txt @@ -1,12 +1,12 @@ I: 5: Locally disabling invalid-name (C0103) I: 5: Suppressed 'invalid-name' (from line 5) -I: 6: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" +I: 6: Pragma "disable-msg" is deprecated, use "disable" instead I: 6: Suppressed 'invalid-name' (from line 6) I: 9: Suppressed 'invalid-name' (from line 8) -I: 12: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" +I: 12: Pragma "disable-msg" is deprecated, use "disable" instead I: 13: Suppressed 'invalid-name' (from line 12) -I: 14: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" -I: 16: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" +I: 14: Pragma "enable-msg" is deprecated, use "enable" instead +I: 16: Pragma "disable-msg" is deprecated, use "disable" instead I: 17: Suppressed 'invalid-name' (from line 16) -I: 18: Deprecated pragma "pylint:disable-msg" or "pylint:enable-msg" +I: 18: Pragma "enable-msg" is deprecated, use "enable" instead I: 21: Suppressed 'invalid-name' (from line 20) -- cgit v1.2.1 From 3a8f94025d14cbd2d9547cdc75611ad6f105e67d Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 25 Apr 2014 07:36:47 +0200 Subject: restore init-hook accidentaly renamed in cset 474324137864. Closes #211 --- ChangeLog | 3 +++ lint.py | 2 +- test/unittest_lint.py | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8bb3ba9..93ba5a7 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,9 @@ ChangeLog for Pylint * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. + * Restore --init-hook, renamed accidentally into --init-hooks in 1.2.0 + (#211) + 2014-04-18 -- 1.2.0 * Pass the current python paths to pylint process when invoked via epylint. Fixes BitBucket issue #133. diff --git a/lint.py b/lint.py index 49eea73..679ee69 100644 --- a/lint.py +++ b/lint.py @@ -907,7 +907,7 @@ group are mutually exclusive.'), try: preprocess_options(args, { # option: (callback, takearg) - 'init-hooks': (cb_init_hook, True), + 'init-hook': (cb_init_hook, True), 'rcfile': (self.cb_set_rcfile, True), 'load-plugins': (self.cb_add_plugins, True), }) diff --git a/test/unittest_lint.py b/test/unittest_lint.py index a7213bd..2b13039 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -381,9 +381,9 @@ class PyLinterTC(TestCase): def test_init_hooks_called_before_load_plugins(self): self.assertRaises(RuntimeError, - Run, ['--load-plugins', 'unexistant', '--init-hooks', 'raise RuntimeError']) + Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError']) self.assertRaises(RuntimeError, - Run, ['--init-hooks', 'raise RuntimeError', '--load-plugins', 'unexistant']) + Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant']) class ConfigTC(TestCase): -- cgit v1.2.1 From 4ec1bf99b1dabed62cbaa981a94f69df0d78613a Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 14:00:21 +0200 Subject: Add a check for indentation of continued lines, enforcing PEP8 style of continued and hanging indents. --- ChangeLog | 2 + __pkginfo__.py | 4 +- checkers/format.py | 362 +++++++++++++++++++++-- test/input/func_bad_continuation.py | 222 ++++++++++++++ test/input/func_format_py27.py | 3 +- test/input/func_format_py_27.py | 3 +- test/input/func_noerror_new_style_class_py_30.py | 2 +- test/messages/func_bad_continuation.txt | 70 +++++ test/messages/func_format_py27.txt | 6 +- test/unittest_checker_format.py | 2 +- 10 files changed, 647 insertions(+), 29 deletions(-) create mode 100644 test/input/func_bad_continuation.py create mode 100644 test/messages/func_bad_continuation.txt diff --git a/ChangeLog b/ChangeLog index 93ba5a7..679a7ba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,8 @@ ChangeLog for Pylint ==================== -- + * Add a new warning [bad-continuation] for badly indentend continued + lines. * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. diff --git a/__pkginfo__.py b/__pkginfo__.py index ea67fe6..d0a9aac 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -45,8 +45,8 @@ classifiers = ['Development Status :: 4 - Beta', 'Programming Language :: Python :: 3', 'Topic :: Software Development :: Debuggers', 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - ] + 'Topic :: Software Development :: Testing' + ] long_desc = """\ diff --git a/checkers/format.py b/checkers/format.py index 7ab6c7e..c957816 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -35,6 +35,7 @@ from pylint.checkers import BaseTokenChecker from pylint.checkers.utils import check_messages from pylint.utils import WarningScope, OPTION_RGX +_CONTINUATION_BLOCK_OPENERS = ['elif', 'except', 'for', 'if', 'while', 'def', 'class'] _KEYWORD_TOKENS = ['assert', 'del', 'elif', 'except', 'for', 'if', 'in', 'not', 'raise', 'return', 'while', 'yield'] if sys.version_info < (3, 0): @@ -45,6 +46,7 @@ _SPACED_OPERATORS = ['==', '<', '>', '!=', '<>', '<=', '>=', '%=', '>>=', '<<='] _OPENING_BRACKETS = ['(', '[', '{'] _CLOSING_BRACKETS = [')', ']', '}'] +_TAB_LENGTH = 8 _EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT]) @@ -77,6 +79,9 @@ MSGS = { 'bad-indentation', 'Used when an unexpected number of indentation\'s tabulations or ' 'spaces has been found.'), + 'C0330': ('Wrong %s indentation%s.\n%s%s', + 'bad-continuation', + 'TODO'), 'W0312': ('Found indentation with %ss instead of %ss', 'mixed-indentation', 'Used when there are some mixed tabs and spaces in a module.'), @@ -138,6 +143,254 @@ def _column_distance(token1, token2): return token2[2][1] - token1[3][1] +def _token_followed_by_eol(tokens, position): + return (tokens.type(position+1) == tokenize.NL or + tokens.type(position+1) == tokenize.COMMENT and + tokens.type(position+2) == tokenize.NL) + + +def _get_indent_length(line): + """Return the length of the indentation on the given token's line.""" + result = 0 + for char in line: + if char == ' ': + result += 1 + elif char == '\t': + result += _TAB_LENGTH + else: + break + return result + + +def _get_indent_hint_line(bar_positions, bad_position): + """Return a line with |s for each of the positions in the given lists.""" + if not bar_positions: + return '' + markers = [(pos, '|') for pos in bar_positions] + markers.append((bad_position, '^')) + markers.sort() + line = [' '] * (markers[-1][0] + 1) + for position, marker in markers: + line[position] = marker + return ''.join(line) + + +class _ContinuedIndent(object): + __slots__ = ('valid_outdent_offsets', + 'valid_continuation_offsets', + 'context_type', + 'token', + 'position') + + def __init__(self, + context_type, + token, + position, + valid_outdent_offsets, + valid_continuation_offsets): + self.valid_outdent_offsets = valid_outdent_offsets + self.valid_continuation_offsets = valid_continuation_offsets + self.context_type = context_type + self.position = position + self.token = token + + +# The contexts for hanging indents. +# A hanging indented dictionary value after : +HANGING_DICT_VALUE = 'dict-value' +# Hanging indentation in an expression. +HANGING = 'hanging' +# Hanging indentation in a block header. +HANGING_BLOCK = 'hanging-block' +# Continued indentation inside an expression. +CONTINUED = 'continued' +# Continued indentation in a block header. +CONTINUED_BLOCK = 'continued-block' + +SINGLE_LINE = 'single' +WITH_BODY = 'multi' + +_CONTINUATION_MSG_PARTS = { + HANGING_DICT_VALUE: ('hanging', ' in dict value'), + HANGING: ('hanging', ''), + HANGING_BLOCK: ('hanging', ' before block'), + CONTINUED: ('continued', ''), + CONTINUED_BLOCK: ('continued', ' before block'), +} + + +def _Offsets(*args): + """Valid indentation offsets for a continued line.""" + return {a: None for a in args} + + +def _BeforeBlockOffsets(single, with_body): + """Valid alternative indent offsets for continued lines before blocks. + + :param single: Valid offset for statements on a single logical line. + :param with_body: Valid offset for statements on several lines. + """ + return {single: SINGLE_LINE, with_body: WITH_BODY} + + +class TokenWrapper(object): + """A wrapper for readable access to token information.""" + + def __init__(self, tokens): + self._tokens = tokens + + def token(self, idx): + return self._tokens[idx][1] + + def type(self, idx): + return self._tokens[idx][0] + + def start_line(self, idx): + return self._tokens[idx][2][0] + + def start_col(self, idx): + return self._tokens[idx][2][1] + + def line(self, idx): + return self._tokens[idx][4] + + +class ContinuedLineState(object): + """Tracker for continued indentation inside a logical line.""" + + def __init__(self, tokens, config): + self._line_start = -1 + self._cont_stack = [] + self._is_block_opener = False + self.retained_warnings = [] + self._config = config + self._tokens = TokenWrapper(tokens) + + @property + def has_content(self): + return bool(self._cont_stack) + + @property + def _block_indent_size(self): + return len(self._config.indent_string.replace('\t', ' ' * _TAB_LENGTH)) + + @property + def _continuation_size(self): + return self._config.indent_after_paren + + def handle_line_start(self, pos): + """Record the first non-junk token at the start of a line.""" + if self._line_start > -1: + return + self._is_block_opener = self._tokens.token(pos) in _CONTINUATION_BLOCK_OPENERS + self._line_start = pos + + def next_physical_line(self): + """Prepares the tracker for a new physical line (NL).""" + self._line_start = -1 + self._is_block_opener = False + + def next_logical_line(self): + """Prepares the tracker for a new logical line (NEWLINE). + + A new logical line only starts with block indentation. + """ + self.next_physical_line() + self.retained_warnings = [] + self._cont_stack = [] + + def add_block_warning(self, token_position, state, valid_offsets): + self.retained_warnings.append((token_position, state, valid_offsets)) + + def get_valid_offsets(self, idx): + """"Returns the valid offsets for the token at the given position.""" + # The closing brace on a dict or the 'for' in a dict comprehension may + # reset two indent levels because the dict value is ended implicitly + stack_top = -1 + if self._tokens.token(idx) in ('}', 'for') and self._cont_stack[-1].token == ':': + stack_top = -2 + indent = self._cont_stack[stack_top] + if self._tokens.token(idx) in _CLOSING_BRACKETS: + valid_offsets = indent.valid_outdent_offsets + else: + valid_offsets = indent.valid_continuation_offsets + return indent, valid_offsets.copy() + + def _hanging_indent_after_bracket(self, bracket, position): + """Extracts indentation information for a hanging indent.""" + indentation = _get_indent_length(self._tokens.line(position)) + if self._is_block_opener and self._continuation_size == self._block_indent_size: + return _ContinuedIndent( + HANGING_BLOCK, + bracket, + position, + _Offsets(indentation + self._continuation_size, indentation), + _BeforeBlockOffsets(indentation + self._continuation_size, + indentation + self._continuation_size * 2)) + elif bracket == ':': + if self._cont_stack[-1].context_type == CONTINUED: + # If the dict key was on the same line as the open brace, the new + # correct indent should be relative to the key instead of the + # current indent level + paren_align = self._cont_stack[-1].valid_outdent_offsets + next_align = self._cont_stack[-1].valid_continuation_offsets.copy() + next_align[next_align.keys()[0] + self._continuation_size] = True + else: + next_align = _Offsets(indentation + self._continuation_size, indentation) + paren_align = _Offsets(indentation + self._continuation_size, indentation) + return _ContinuedIndent(HANGING_DICT_VALUE, bracket, position, paren_align, next_align) + else: + return _ContinuedIndent( + HANGING, + bracket, + position, + _Offsets(indentation, indentation + self._continuation_size), + _Offsets(indentation + self._continuation_size)) + + def _continuation_inside_bracket(self, bracket, pos): + """Extracts indentation information for a continued indent.""" + indentation = _get_indent_length(self._tokens.line(pos)) + if self._is_block_opener and self._tokens.start_col(pos+1) - indentation == self._block_indent_size: + return _ContinuedIndent( + CONTINUED_BLOCK, + bracket, + pos, + _Offsets(self._tokens.start_col(pos)), + _BeforeBlockOffsets(self._tokens.start_col(pos+1), + self._tokens.start_col(pos+1) + self._continuation_size)) + else: + return _ContinuedIndent( + CONTINUED, + bracket, + pos, + _Offsets(self._tokens.start_col(pos)), + _Offsets(self._tokens.start_col(pos+1))) + + def pop_token(self): + self._cont_stack.pop() + + def push_token(self, token, position): + """Pushes a new token for continued indentation on the stack. + + Tokens that can modify continued indentation offsets are: + * opening brackets + * 'lambda' + * : inside dictionaries + + push_token relies on the caller to filter out those + interesting tokens. + + :param token: The concrete token + :param position: The position of the token in the stream. + """ + if _token_followed_by_eol(self._tokens, position): + self._cont_stack.append( + self._hanging_indent_after_bracket(token, position)) + else: + self._cont_stack.append( + self._continuation_inside_bracket(token, position)) + + class FormatChecker(BaseTokenChecker): """checks for : * unauthorized constructions @@ -180,11 +433,25 @@ class FormatChecker(BaseTokenChecker): {'default' : ' ', 'type' : "string", 'metavar' : '', 'help' : 'String used as indentation unit. This is usually \ " " (4 spaces) or "\\t" (1 tab).'}), + ('indent-after-paren', + {'type': 'int', 'metavar': '', 'default': 4, + 'help': 'Number of spaces of indent required inside a hanging ' + ' or continued line.'}), ) + def __init__(self, linter=None): BaseTokenChecker.__init__(self, linter) self._lines = None self._visited_lines = None + self._bracket_stack = [None] + + def _pop_token(self): + self._bracket_stack.pop() + self._current_line.pop_token() + + def _push_token(self, token, idx): + self._bracket_stack.append(token) + self._current_line.push_token(token, idx) def new_line(self, tok_type, line, line_num, junk): """a new line has been encountered, process it if necessary""" @@ -211,6 +478,8 @@ class FormatChecker(BaseTokenChecker): start: int; the position of the keyword in the token list. """ # If the next token is not a paren, we're fine. + if self._inside_brackets(':') and tokens[start][1] == 'for': + self._pop_token() if tokens[start+1][1] != '(': return @@ -270,7 +539,7 @@ class FormatChecker(BaseTokenChecker): return def _opening_bracket(self, tokens, i): - self._bracket_stack.append(tokens[i][1]) + self._push_token(tokens[i][1], i) # Special case: ignore slices if tokens[i][1] == '[' and tokens[i+1][1] == ':': return @@ -283,7 +552,9 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, (_IGNORE, _MUST_NOT)) def _closing_bracket(self, tokens, i): - self._bracket_stack.pop() + if self._inside_brackets(':'): + self._pop_token() + self._pop_token() # Special case: ignore slices if tokens[i-1][1] == ':' and tokens[i][1] == ']': return @@ -302,7 +573,7 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, (_MUST, _MUST)) def _open_lambda(self, tokens, i): # pylint:disable=unused-argument - self._bracket_stack.append('lambda') + self._push_token('lambda', i) def _handle_colon(self, tokens, i): # Special case: ignore slices @@ -316,7 +587,9 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, policy) if self._inside_brackets('lambda'): - self._bracket_stack.pop() + self._pop_token() + elif self._inside_brackets('{'): + self._push_token(':', i) def _handle_comma(self, tokens, i): # Only require a following whitespace if this is @@ -325,6 +598,8 @@ class FormatChecker(BaseTokenChecker): self._check_space(tokens, i, (_MUST_NOT, _IGNORE)) else: self._check_space(tokens, i, (_MUST_NOT, _MUST)) + if self._inside_brackets(':'): + self._pop_token() def _check_surrounded_by_space(self, tokens, i): """Check that a binary operator is surrounded by exactly one space.""" @@ -419,10 +694,10 @@ class FormatChecker(BaseTokenChecker): regular expression). """ self._bracket_stack = [None] - indent = tokenize.INDENT - dedent = tokenize.DEDENT - newline = tokenize.NEWLINE + # TODO(tmarek): extra to toplevel junk = (tokenize.COMMENT, tokenize.NL) + indent_junk = set(junk) | set([tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT]) + indents = [0] check_equal = 0 line_num = 0 @@ -431,6 +706,8 @@ class FormatChecker(BaseTokenChecker): self._visited_lines = {} new_line_delay = False token_handlers = self._prepare_token_dispatcher() + + self._current_line = ContinuedLineState(tokens, self.config) for idx, (tok_type, token, start, _, line) in enumerate(tokens): if new_line_delay: new_line_delay = False @@ -448,9 +725,9 @@ class FormatChecker(BaseTokenChecker): new_line_delay = True else: self.new_line(tok_type, line, line_num, junk) - if tok_type not in (indent, dedent, newline) + junk: + if tok_type not in indent_junk: previous = tok_type, token, start[0] - + self._current_line.handle_line_start(idx) if tok_type == tokenize.OP: if token == '<>': self.add_message('old-ne-operator', line=line_num) @@ -458,28 +735,31 @@ class FormatChecker(BaseTokenChecker): if token.endswith('l'): self.add_message('lowercase-l-suffix', line=line_num) - elif tok_type == newline: + elif tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form # (NL | COMMENT)* (INDENT | DEDENT+)? # If an INDENT appears, setting check_equal is wrong, and will # be undone when we see the INDENT. - check_equal = 1 + check_equal = True - elif tok_type == indent: - check_equal = 0 + self._process_retained_warnings(TokenWrapper(tokens), idx) + self._current_line.next_logical_line() + elif tok_type == tokenize.INDENT: + check_equal = False self.check_indent_level(token, indents[-1]+1, line_num) indents.append(indents[-1]+1) - - elif tok_type == dedent: + elif tok_type == tokenize.DEDENT: # there's nothing we need to check here! what's important is # that when the run of DEDENTs ends, the indentation of the # program statement (or ENDMARKER) that triggered the run is # equal to what's left at the top of the indents stack - check_equal = 1 + check_equal = True if len(indents) > 1: del indents[-1] - + elif tok_type == tokenize.NL: + self._check_continued_indentation(TokenWrapper(tokens), idx+1) + self._current_line.next_physical_line() elif check_equal and tok_type not in junk: # this is the first "real token" following a NEWLINE, so it # must be the first token of the next program statement, or an @@ -487,7 +767,7 @@ class FormatChecker(BaseTokenChecker): # for this statement; in the case of ENDMARKER, line is an empty # string, so will properly match the empty string with which the # "indents" stack was seeded - check_equal = 0 + check_equal = False self.check_indent_level(line, indents[-1], line_num) try: @@ -501,6 +781,52 @@ class FormatChecker(BaseTokenChecker): if line_num > self.config.max_module_lines: self.add_message('too-many-lines', args=line_num, line=1) + def _process_retained_warnings(self, tokens, current_pos): + single_line_block_stmt = not ( + tokens.token(current_pos-1) == ':' or + current_pos > 1 and tokens.token(current_pos-2) == ':' + and tokens.type(current_pos-1) == tokenize.COMMENT) + + for indent_pos, state, offsets in self._current_line.retained_warnings: + block_type = offsets[tokens.start_col(indent_pos)] + hints = dict((k, v) for k, v in offsets.iteritems() + if v != block_type) + if single_line_block_stmt and block_type == WITH_BODY: + self._add_continuation_message(state, hints, tokens, indent_pos) + elif not single_line_block_stmt and block_type == SINGLE_LINE: + self._add_continuation_message(state, hints, tokens, indent_pos) + + def _check_continued_indentation(self, tokens, next_idx): + # Do not issue any warnings if the next line is empty. + if not self._current_line.has_content or tokens.type(next_idx) == tokenize.NL: + return + + state, valid_offsets = self._current_line.get_valid_offsets(next_idx) + # Special handling for hanging comments. If the last line ended with a + # comment and the new line contains only a comment, the line may also be + # indented to the start of the previous comment. + if (tokens.type(next_idx) == tokenize.COMMENT and + tokens.type(next_idx-2) == tokenize.COMMENT): + valid_offsets[tokens.start_col(next_idx-2)] = True + + # We can only decide if the indentation of a continued line before opening + # a new block is valid once we know of the body of the block is on the + # same line as the block opener. Since the token processing is single-pass, + # emitting those warnings is delayed until the block opener is processed. + if (state.context_type in (HANGING_BLOCK, CONTINUED_BLOCK) + and tokens.start_col(next_idx) in valid_offsets): + self._current_line.add_block_warning(next_idx, state, valid_offsets) + elif tokens.start_col(next_idx) not in valid_offsets: + self._add_continuation_message(state, valid_offsets, tokens, next_idx) + + def _add_continuation_message(self, state, offsets, tokens, position): + readable_type, readable_position = _CONTINUATION_MSG_PARTS[state.context_type] + hint_line = _get_indent_hint_line(offsets, tokens.start_col(position)) + self.add_message( + 'bad-continuation', + line=tokens.start_line(position), + args=(readable_type, readable_position, tokens.line(position), hint_line)) + @check_messages('multiple-statements') def visit_default(self, node): """check the node line number and check it if not yet done""" diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py new file mode 100644 index 0000000..f0787de --- /dev/null +++ b/test/input/func_bad_continuation.py @@ -0,0 +1,222 @@ +"""Regression test case for bad-continuation.""" + +__revision__ = 1 + +# Various alignment for brackets +print [ + 1, 2, 3 +] +print [ + 1, 2, 3 + ] +print [ + 1, 2, 3 + ] # [bad-continuation] + +# Alignment inside literals +W0 = [1, 2, 3, + 4, 5, 6, + 7, # [bad-continuation] + 8, 9, 10, + 11, 12, 13, + # and a comment + 14, 15, 16] + +W1 = { + 'a': 1, + 'b': 2, # [bad-continuation] + 'c': 3, + } + +W2 = { + 'a': 1, + 'b': 2, # [bad-continuation] + 'c': 3, + } + +W2 = ['some', 'contents' # with a continued comment that may be aligned + # under the previous comment (optionally) + 'and', + 'more', # but this + # [bad-continuation] is not accepted + 'contents', # nor this. [bad-continuation] + ] + +# Values in dictionaries should be indented 4 spaces further if they are on a +# different line than their key +W4 = { + 'key1': + 'value1', # Grandfather in the old style + 'key2': + 'value2', # [bad-continuation] + 'key3': + 'value3', # Comma here + } + +# And should follow the same rules as continuations within parens +W5 = { + 'key1': 'long value' + 'long continuation', + 'key2': 'breaking' + 'wrong', # [bad-continuation] + 'key3': 2*( + 2+2), + 'key4': ('parenthesis', + 'continuation') # No comma here + } + +# Allow values to line up with their keys when the key is next to the brace +W6 = {'key1': + 'value1', + 'key2': + 'value2', + } + +# Or allow them to be indented +W7 = {'key1': + 'value1', + 'key2': + 'value2' + } + +# Bug that caused a warning on the previous two cases permitted these odd +# incorrect indentations +W8 = {'key1': +'value1', # [bad-continuation] + } + +W9 = {'key1': + 'value1', # [bad-continuation] + } + +# Dictionary comprehensions should not require extra indentation when breaking +# before the 'for', which is not part of the value +C1 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} + +C2 = {'key{}'.format(x): 'value{}'.format(x) for x in + range(3)} + +# Dictionary comprehensions with multiple loops broken in different places +C3 = {x*y: (x, y) for x in range(3) for y in range(3)} + +C4 = {x*y: (x, y) + for x in range(3) for y in range(3)} + +C5 = {x*y: (x, y) for x + in range(3) for y in range(3)} + +C6 = {x*y: (x, y) for x in range(3) + for y in range(3)} + +C7 = {key: + key ** 2 + for key in range(10)} + +C8 = { + key: key ** 2 + for key in range(10)} + +# Misaligned cases for dict comprehensions +C9 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} # [bad-continuation] + +C9 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} # [bad-continuation] + +# Alignment of arguments in function definitions +def continue1(some_arg, + some_other_arg): + """A function with well-aligned arguments.""" + print some_arg, some_other_arg + + +def continue2( + some_arg, + some_other_arg): + """A function with well-aligned arguments.""" + print some_arg, some_other_arg + +def continue3( + some_arg, # [bad-continuation] + some_other_arg): # [bad-continuation] + """A function with misaligned arguments""" + print some_arg, some_other_arg + +def continue4( + arg1, + arg2): print arg1, arg2 + + +def callee(*args): + """noop""" + print args + + +callee( + "a", + "b" + ) + +callee("a", + "b") # [bad-continuation] + +callee(5, {'a': 'b', + 'c': 'd'}) + +if ( + 1 + ): pass + +if ( + 1 +): pass +if ( + 1 + ): pass # [bad-continuation] + +if (1 and + 2): # [bad-continuation] + pass + +while (1 and + 2): + pass + +while (1 and + 2 and # [bad-continuation] + 3): + pass + +if ( + 2): pass # [bad-continuation] + +if (1 or + 2 or + 3): pass + +if (1 or + 2 or # [bad-continuation] + 3): print 1, 2 + +if (1 and + 2): pass # [bad-continuation] + +if ( + 2): pass + +if ( + 2): # [bad-continuation] + pass + +L1 = (lambda a, + b: a + b) + +if not (1 and + 2): + print 3 + +if not (1 and + 2): # [bad-continuation] + print 3 + diff --git a/test/input/func_format_py27.py b/test/input/func_format_py27.py index 828a180..46d5cfe 100644 --- a/test/input/func_format_py27.py +++ b/test/input/func_format_py27.py @@ -79,8 +79,7 @@ def hop(context): return [''' %(send)s%(send)s''' % context, ''' -%(cancel)s%(cancel)s''' % context, - ] +%(cancel)s%(cancel)s''' % context] titreprojet = '\ Drapeau vert\ %s' % aaaa diff --git a/test/input/func_format_py_27.py b/test/input/func_format_py_27.py index f062f27..95f7fde 100644 --- a/test/input/func_format_py_27.py +++ b/test/input/func_format_py_27.py @@ -79,8 +79,7 @@ def hop(context): return [''' %(send)s%(send)s''' % context, ''' -%(cancel)s%(cancel)s''' % context, - ] +%(cancel)s%(cancel)s''' % context] titreprojet = '\ Drapeau vert\ %s' % aaaa diff --git a/test/input/func_noerror_new_style_class_py_30.py b/test/input/func_noerror_new_style_class_py_30.py index 87bfb9e..9e55277 100644 --- a/test/input/func_noerror_new_style_class_py_30.py +++ b/test/input/func_noerror_new_style_class_py_30.py @@ -18,7 +18,7 @@ class File(file): super(File, self).__init__(name, mode, buffering) if self.verbose: print "File %s is opened. The mode is: %s" % (self.name, -self.mode) + self.mode) # def write(self, a_string): diff --git a/test/messages/func_bad_continuation.txt b/test/messages/func_bad_continuation.txt new file mode 100644 index 0000000..ba6516d --- /dev/null +++ b/test/messages/func_bad_continuation.txt @@ -0,0 +1,70 @@ +C: 14: Wrong hanging indentation. + ] # [bad-continuation] +| ^| +C: 19: Wrong continued indentation. + 7, # [bad-continuation] + | ^ +C: 27: Wrong hanging indentation. + 'b': 2, # [bad-continuation] + ^| +C: 33: Wrong hanging indentation. + 'b': 2, # [bad-continuation] + ^| +C: 41: Wrong continued indentation. + # [bad-continuation] is not accepted + | | ^ +C: 42: Wrong continued indentation. + 'contents', # nor this. [bad-continuation] + | ^ +C: 51: Wrong hanging indentation in dict value. + 'value2', # [bad-continuation] + | ^ | +C: 61: Wrong continued indentation. + 'wrong', # [bad-continuation] + ^ | +C: 85: Wrong hanging indentation in dict value. +'value1', # [bad-continuation] +^ | | +C: 89: Wrong hanging indentation in dict value. + 'value1', # [bad-continuation] + ^ | | +C:122: Wrong continued indentation. + for x in range(3)} # [bad-continuation] + ^ | +C:125: Wrong continued indentation. + for x in range(3)} # [bad-continuation] + | ^ +C:141: Wrong hanging indentation before block. + some_arg, # [bad-continuation] + ^ | +C:142: Wrong hanging indentation before block. + some_other_arg): # [bad-continuation] + ^ | +C:146:continue4: Missing function docstring +C:162: Wrong continued indentation. + "b") # [bad-continuation] + ^ | +C:176: Wrong hanging indentation before block. + ): pass # [bad-continuation] +| ^| +C:179: Wrong continued indentation before block. + 2): # [bad-continuation] + ^ | +C:187: Wrong continued indentation. + 2 and # [bad-continuation] + | ^ +C:192: Wrong hanging indentation before block. + 2): pass # [bad-continuation] + ^ | | +C:199: Wrong continued indentation before block. + 2 or # [bad-continuation] + |^ | +C:203: Wrong continued indentation before block. + 2): pass # [bad-continuation] + ^ | | +C:209: Wrong hanging indentation before block. + 2): # [bad-continuation] + ^ | | +C:220: Wrong continued indentation. + 2): # [bad-continuation] + ^ | diff --git a/test/messages/func_format_py27.txt b/test/messages/func_format_py27.txt index ce5df81..f71cee8 100644 --- a/test/messages/func_format_py27.txt +++ b/test/messages/func_format_py27.txt @@ -41,6 +41,6 @@ C: 71: Exactly one space required before assignment C: 73: Exactly one space required around assignment ocount[obj.__class__]=1 ^ -C: 91: More than one statement on a single line -C:105: More than one statement on a single line -C:108: More than one statement on a single line +C: 90: More than one statement on a single line +C:104: More than one statement on a single line +C:107: More than one statement on a single line diff --git a/test/unittest_checker_format.py b/test/unittest_checker_format.py index 8f1be78..b9fba5c 100644 --- a/test/unittest_checker_format.py +++ b/test/unittest_checker_format.py @@ -135,7 +135,7 @@ class CheckSpaceTest(CheckerTestCase): good_cases = [ '(a)\n', '(a * (b + c))\n', - '( #\na)\n', + '(#\n a)\n', ] with self.assertNoMessages(): for code in good_cases: -- cgit v1.2.1 From ec76d962a282fdc68045ef81f9c6602d7734e436 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 17 Apr 2014 19:14:43 +0200 Subject: Do not crash in update mode if a file does not exist. --- testutils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/testutils.py b/testutils.py index d60ab46..8c01ee6 100644 --- a/testutils.py +++ b/testutils.py @@ -312,9 +312,11 @@ class LintTestUpdate(LintTestUsingModule): 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,)) + try: + expected = self._get_expected() + except IOError: + expected = '' + if got != expected: with open(self.output, 'w') as fobj: fobj.write(got) -- cgit v1.2.1 From 5e39be38b66d9063bbfb68e1b4fc61b32a5b0a66 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 18 Apr 2014 09:27:01 +0200 Subject: [test] rename test modules --- test/test_base.py | 223 -------------------------------- test/test_format.py | 257 ------------------------------------- test/test_logging.py | 49 ------- test/test_misc.py | 86 ------------- test/test_similar.py | 142 -------------------- test/test_type.py | 42 ------ test/test_utils.py | 63 --------- test/unittest_checker_base.py | 223 ++++++++++++++++++++++++++++++++ test/unittest_checker_format.py | 257 +++++++++++++++++++++++++++++++++++++ test/unittest_checker_logging.py | 49 +++++++ test/unittest_checker_misc.py | 71 ++++++++++ test/unittest_checker_similar.py | 142 ++++++++++++++++++++ test/unittest_checker_typecheck.py | 42 ++++++ test/unittest_checker_variables.py | 24 +++- test/unittest_utils.py | 63 +++++++++ 15 files changed, 870 insertions(+), 863 deletions(-) delete mode 100644 test/test_base.py delete mode 100644 test/test_format.py delete mode 100644 test/test_logging.py delete mode 100644 test/test_misc.py delete mode 100644 test/test_similar.py delete mode 100644 test/test_type.py delete mode 100644 test/test_utils.py create mode 100644 test/unittest_checker_base.py create mode 100644 test/unittest_checker_format.py create mode 100644 test/unittest_checker_logging.py create mode 100644 test/unittest_checker_misc.py create mode 100644 test/unittest_checker_similar.py create mode 100644 test/unittest_checker_typecheck.py create mode 100644 test/unittest_utils.py diff --git a/test/test_base.py b/test/test_base.py deleted file mode 100644 index 972c783..0000000 --- a/test/test_base.py +++ /dev/null @@ -1,223 +0,0 @@ -"""Unittest for the base checker.""" - -import re - -from astroid import test_utils -from pylint.checkers import base -from pylint.testutils import CheckerTestCase, Message, set_config - - -class DocstringTest(CheckerTestCase): - CHECKER_CLASS = base.DocStringChecker - - 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 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 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) - - @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) - - @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 test_class_no_docstring(self): - klass = test_utils.extract_node(""" - class Klass(object): - pass""") - with self.assertAddsMessages(Message('missing-docstring', node=klass, args=('class',))): - self.checker.visit_class(klass) - - -class NameCheckerTest(CheckerTestCase): - CHECKER_CLASS = base.NameChecker - CONFIG = { - 'bad_names': set(), - } - - @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. - methods = test_utils.extract_node(""" - import abc - - class FooClass(object): - @property - def FOO(self): #@ - pass - - @property - def bar(self): #@ - pass - - @abc.abstractproperty - def BAZ(self): #@ - pass - """) - with self.assertNoMessages(): - self.checker.visit_function(methods[0]) - self.checker.visit_function(methods[2]) - with self.assertAddsMessages(Message('invalid-name', node=methods[1], - args=('attribute', 'bar', ''))): - self.checker.visit_function(methods[1]) - - @set_config(attr_rgx=re.compile('[A-Z]+')) - def test_property_setters(self): - method = test_utils.extract_node(""" - class FooClass(object): - @property - def foo(self): pass - - @foo.setter - def FOOSETTER(self): #@ - pass - """) - with self.assertNoMessages(): - self.checker.visit_function(method) - - def test_module_level_names(self): - assign = test_utils.extract_node(""" - import collections - Class = collections.namedtuple("a", ("b", "c")) #@ - """) - with self.assertNoMessages(): - self.checker.visit_assname(assign.targets[0]) - - assign = test_utils.extract_node(""" - class ClassA(object): - pass - ClassB = ClassA - """) - with self.assertNoMessages(): - self.checker.visit_assname(assign.targets[0]) - - module = test_utils.build_module(""" - def A(): - return 1, 2, 3 - CONSTA, CONSTB, CONSTC = A() - CONSTD = A()""") - with self.assertNoMessages(): - self.checker.visit_assname(module.body[1].targets[0].elts[0]) - self.checker.visit_assname(module.body[2].targets[0]) - - assign = test_utils.extract_node(""" - CONST = "12 34 ".rstrip().split()""") - with self.assertNoMessages(): - self.checker.visit_assname(assign.targets[0]) - - -class MultiNamingStyleTest(CheckerTestCase): - CHECKER_CLASS = base.NameChecker - - MULTI_STYLE_RE = re.compile('(?:(?P[A-Z]+)|(?P[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('(?:(?PFOO)|(?P[A-Z]+)|(?P[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 deleted file mode 100644 index 8f1be78..0000000 --- a/test/test_format.py +++ /dev/null @@ -1,257 +0,0 @@ -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 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 - -Check format checker helper functions -""" - -import sys -import re -from os import linesep -import tokenize -import StringIO - -from logilab.common.testlib import TestCase, unittest_main -from astroid import test_utils - -from pylint.checkers.format import * - -from pylint.testutils import CheckerTestCase, Message, set_config - - -def tokenize_str(code): - return list(tokenize.generate_tokens(StringIO.StringIO(code).readline)) - - -class MultiStatementLineTest(CheckerTestCase): - CHECKER_CLASS = FormatChecker - - def testSingleLineIfStmts(self): - stmt = test_utils.extract_node(""" - if True: pass #@ - """) - with self.assertAddsMessages(Message('multiple-statements', node=stmt.body[0])): - self.checker.process_tokens([]) - self.checker.visit_default(stmt.body[0]) - self.checker.config.single_line_if_stmt = True - with self.assertNoMessages(): - self.checker.process_tokens([]) - self.checker.visit_default(stmt.body[0]) - stmt = test_utils.extract_node(""" - if True: pass #@ - else: - pass - """) - with self.assertAddsMessages(Message('multiple-statements', node=stmt.body[0])): - self.checker.process_tokens([]) - self.checker.visit_default(stmt.body[0]) - - def testTryExceptFinallyNoMultipleStatement(self): - tree = test_utils.extract_node(""" - try: #@ - pass - except: - pass - finally: - pass""") - with self.assertNoMessages(): - self.checker.process_tokens([]) - self.checker.visit_default(tree.body[0]) - - - -class SuperfluousParenthesesTest(CheckerTestCase): - CHECKER_CLASS = FormatChecker - - def testCheckKeywordParensHandlesValidCases(self): - self.checker._keywords_with_parens = set() - cases = [ - 'if foo:', - 'if foo():', - 'if (x and y) or z:', - 'assert foo()', - 'assert ()', - 'if (1, 2) in (3, 4):', - 'if (a or b) in c:', - 'return (x for x in x)', - 'if (x for x in x):', - 'for x in (x for x in x):', - 'not (foo or bar)', - 'not (foo or bar) and baz', - ] - with self.assertNoMessages(): - for code in cases: - self.checker._check_keyword_parentheses(tokenize_str(code), 0) - - def testCheckKeywordParensHandlesUnnecessaryParens(self): - self.checker._keywords_with_parens = set() - cases = [ - (Message('superfluous-parens', line=1, args='if'), - 'if (foo):', 0), - (Message('superfluous-parens', line=1, args='if'), - 'if ((foo, bar)):', 0), - (Message('superfluous-parens', line=1, args='if'), - 'if (foo(bar)):', 0), - (Message('superfluous-parens', line=1, args='return'), - 'return ((x for x in x))', 0), - (Message('superfluous-parens', line=1, args='not'), - 'not (foo)', 0), - (Message('superfluous-parens', line=1, args='not'), - 'if not (foo):', 1), - (Message('superfluous-parens', line=1, args='if'), - 'if (not (foo)):', 0), - (Message('superfluous-parens', line=1, args='not'), - 'if (not (foo)):', 2), - ] - for msg, code, offset in cases: - with self.assertAddsMessages(msg): - self.checker._check_keyword_parentheses(tokenize_str(code), offset) - - def testFuturePrintStatementWithoutParensWarning(self): - code = """from __future__ import print_function -print('Hello world!') -""" - tree = test_utils.build_module(code) - with self.assertNoMessages(): - self.checker.process_module(tree) - self.checker.process_tokens(tokenize_str(code)) - - -class CheckSpaceTest(CheckerTestCase): - CHECKER_CLASS = FormatChecker - - def testParenthesesGood(self): - good_cases = [ - '(a)\n', - '(a * (b + c))\n', - '( #\na)\n', - ] - with self.assertNoMessages(): - for code in good_cases: - self.checker.process_tokens(tokenize_str(code)) - - def testParenthesesBad(self): - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'after', 'bracket', '( a)\n^'))): - self.checker.process_tokens(tokenize_str('( a)\n')) - - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'before', 'bracket', '(a )\n ^'))): - self.checker.process_tokens(tokenize_str('(a )\n')) - - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'before', 'bracket', 'foo (a)\n ^'))): - self.checker.process_tokens(tokenize_str('foo (a)\n')) - - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'before', 'bracket', '{1: 2} [1]\n ^'))): - self.checker.process_tokens(tokenize_str('{1: 2} [1]\n')) - - def testTrailingCommaGood(self): - with self.assertNoMessages(): - self.checker.process_tokens(tokenize_str('(a, )\n')) - self.checker.process_tokens(tokenize_str('(a,)\n')) - - self.checker.config.no_space_check = [] - with self.assertNoMessages(): - self.checker.process_tokens(tokenize_str('(a,)\n')) - - @set_config(no_space_check=[]) - def testTrailingCommaBad(self): - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'before', 'bracket', '(a, )\n ^'))): - self.checker.process_tokens(tokenize_str('(a, )\n')) - - def testComma(self): - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'before', 'comma', '(a , b)\n ^'))): - self.checker.process_tokens(tokenize_str('(a , b)\n')) - - def testSpacesAllowedInsideSlices(self): - good_cases = [ - '[a:b]\n', - '[a : b]\n', - '[a : ]\n', - '[:a]\n', - '[:]\n', - '[::]\n', - ] - with self.assertNoMessages(): - for code in good_cases: - self.checker.process_tokens(tokenize_str(code)) - - def testKeywordSpacingGood(self): - with self.assertNoMessages(): - self.checker.process_tokens(tokenize_str('foo(foo=bar)\n')) - self.checker.process_tokens(tokenize_str('lambda x=1: x\n')) - - def testKeywordSpacingBad(self): - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'before', 'keyword argument assignment', - '(foo =bar)\n ^'))): - self.checker.process_tokens(tokenize_str('(foo =bar)\n')) - - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'after', 'keyword argument assignment', - '(foo= bar)\n ^'))): - self.checker.process_tokens(tokenize_str('(foo= bar)\n')) - - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('No', 'allowed', 'around', 'keyword argument assignment', - '(foo = bar)\n ^'))): - self.checker.process_tokens(tokenize_str('(foo = bar)\n')) - - def testOperatorSpacingGood(self): - good_cases = [ - 'a = b\n' - 'a < b\n' - 'a\n< b\n', - ] - with self.assertNoMessages(): - for code in good_cases: - self.checker.process_tokens(tokenize_str(code)) - - def testOperatorSpacingBad(self): - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('Exactly one', 'required', 'before', 'comparison', 'a< b\n ^'))): - self.checker.process_tokens(tokenize_str('a< b\n')) - - with self.assertAddsMessages( - Message('bad-whitespace', line=1, - args=('Exactly one', 'required', 'after', 'comparison', 'a = (3, 0): - # erff - os.write(fd, bytes(code, 'ascii')) - else: - os.write(fd, code) - - try: - module = test_utils.build_module(code) - module.file = tmp - yield module - finally: - os.close(fd) - os.remove(tmp) - - -class FixmeTest(CheckerTestCase): - CHECKER_CLASS = misc.EncodingChecker - - def test_fixme(self): - with create_file_backed_module( - """a = 1 - # FIXME """) as module: - with self.assertAddsMessages( - Message(msg_id='fixme', line=2, args=u'FIXME')): - self.checker.process_module(module) - - @set_config(notes=[]) - def test_empty_fixme_regex(self): - with create_file_backed_module( - """a = 1 - # fixme - """) as module: - 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_similar.py b/test/test_similar.py deleted file mode 100644 index d797a5f..0000000 --- a/test/test_similar.py +++ /dev/null @@ -1,142 +0,0 @@ -import sys -from logilab.common.testlib import TestCase, unittest_main - -from cStringIO import StringIO -from os.path import join, dirname, abspath - -from pylint.checkers import similar - -SIMILAR1 = join(dirname(abspath(__file__)), 'input', 'similar1') -SIMILAR2 = join(dirname(abspath(__file__)), 'input', 'similar2') - -class SimilarTC(TestCase): - """test the similar command line utility""" - - def test_ignore_comments(self): - sys.stdout = StringIO() - try: - similar.Run(['--ignore-comments', SIMILAR1, SIMILAR2]) - except SystemExit, ex: - self.assertEqual(ex.code, 0) - output = sys.stdout.getvalue() - else: - self.fail('not system exit') - finally: - sys.stdout = sys.__stdout__ - self.assertMultiLineEqual(output.strip(), (""" -10 similar lines in 2 files -==%s:0 -==%s:0 - import one - from two import two - three - four - five - six - seven - eight - nine - ''' ten -TOTAL lines=44 duplicates=10 percent=22.73 -""" % (SIMILAR1, SIMILAR2)).strip()) - - - def test_ignore_docsrings(self): - sys.stdout = StringIO() - try: - similar.Run(['--ignore-docstrings', SIMILAR1, SIMILAR2]) - except SystemExit, ex: - self.assertEqual(ex.code, 0) - output = sys.stdout.getvalue() - else: - self.fail('not system exit') - finally: - sys.stdout = sys.__stdout__ - self.assertMultiLineEqual(output.strip(), (""" -8 similar lines in 2 files -==%s:6 -==%s:6 - seven - eight - nine - ''' ten - ELEVEN - twelve ''' - thirteen - fourteen - -5 similar lines in 2 files -==%s:0 -==%s:0 - import one - from two import two - three - four - five -TOTAL lines=44 duplicates=13 percent=29.55 -""" % ((SIMILAR1, SIMILAR2) * 2)).strip()) - - - def test_ignore_imports(self): - sys.stdout = StringIO() - try: - similar.Run(['--ignore-imports', SIMILAR1, SIMILAR2]) - except SystemExit, ex: - self.assertEqual(ex.code, 0) - output = sys.stdout.getvalue() - else: - self.fail('not system exit') - finally: - sys.stdout = sys.__stdout__ - self.assertMultiLineEqual(output.strip(), """ -TOTAL lines=44 duplicates=0 percent=0.00 -""".strip()) - - - def test_ignore_nothing(self): - sys.stdout = StringIO() - try: - similar.Run([SIMILAR1, SIMILAR2]) - except SystemExit, ex: - self.assertEqual(ex.code, 0) - output = sys.stdout.getvalue() - else: - self.fail('not system exit') - finally: - sys.stdout = sys.__stdout__ - self.assertMultiLineEqual(output.strip(), (""" -5 similar lines in 2 files -==%s:0 -==%s:0 - import one - from two import two - three - four - five -TOTAL lines=44 duplicates=5 percent=11.36 -""" % (SIMILAR1, SIMILAR2)).strip()) - - def test_help(self): - sys.stdout = StringIO() - try: - similar.Run(['--help']) - except SystemExit, ex: - self.assertEqual(ex.code, 0) - else: - self.fail('not system exit') - finally: - sys.stdout = sys.__stdout__ - - def test_no_args(self): - sys.stdout = StringIO() - try: - similar.Run([]) - except SystemExit, ex: - self.assertEqual(ex.code, 1) - else: - self.fail('not system exit') - finally: - sys.stdout = sys.__stdout__ - -if __name__ == '__main__': - unittest_main() diff --git a/test/test_type.py b/test/test_type.py deleted file mode 100644 index 352b6f9..0000000 --- a/test/test_type.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Unittest for the type checker.""" - -from astroid import test_utils -from pylint.checkers import typecheck -from pylint.testutils import CheckerTestCase, Message, set_config - -class TypeCheckerTest(CheckerTestCase): - "Tests for pylint.checkers.typecheck" - CHECKER_CLASS = typecheck.TypeChecker - - def test_no_member_in_getattr(self): - """Make sure that a module attribute access is checked by pylint. - """ - - node = test_utils.extract_node(""" - import argparse - argparse.THIS_does_not_EXIST - """) - with self.assertAddsMessages( - Message( - 'no-member', - node=node, - args=('Module', 'argparse', 'THIS_does_not_EXIST'))): - self.checker.visit_getattr(node) - - @set_config(ignored_modules=('argparse',)) - def test_no_member_in_getattr_ignored(self): - """Make sure that a module attribute access check is omitted with a - module that is configured to be ignored. - """ - - node = test_utils.extract_node(""" - import argparse - argparse.THIS_does_not_EXIST - """) - with self.assertNoMessages(): - self.checker.visit_getattr(node) - - -if __name__ == '__main__': - from logilab.common.testlib import unittest_main - unittest_main() diff --git a/test/test_utils.py b/test/test_utils.py deleted file mode 100644 index d8c4c67..0000000 --- a/test/test_utils.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2013 Google Inc. -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from logilab.common.testlib import TestCase - -from astroid import test_utils -from pylint import utils -from pylint.checkers.utils import check_messages - - -class PyLintASTWalkerTest(TestCase): - class MockLinter(object): - def __init__(self, msgs): - self._msgs = msgs - - def is_message_enabled(self, msgid): - return self._msgs.get(msgid, True) - - class Checker(object): - def __init__(self): - self.called = set() - - @check_messages('first-message') - def visit_module(self, module): - self.called.add('module') - - @check_messages('second-message') - def visit_callfunc(self, module): - raise NotImplementedError - - @check_messages('second-message', 'third-message') - def visit_assname(self, module): - self.called.add('assname') - - @check_messages('second-message') - def leave_assname(self, module): - raise NotImplementedError - - def testCheckMessages(self): - linter = self.MockLinter({'first-message': True, - 'second-message': False, - 'third-message': True}) - walker = utils.PyLintASTWalker(linter) - checker = self.Checker() - walker.add_checker(checker) - walker.walk(test_utils.build_module("x = func()")) - self.assertEqual(set(['module', 'assname']), checker.called) - - -if __name__ == '__main__': - from logilab.common.testlib import unittest_main - unittest_main() - diff --git a/test/unittest_checker_base.py b/test/unittest_checker_base.py new file mode 100644 index 0000000..972c783 --- /dev/null +++ b/test/unittest_checker_base.py @@ -0,0 +1,223 @@ +"""Unittest for the base checker.""" + +import re + +from astroid import test_utils +from pylint.checkers import base +from pylint.testutils import CheckerTestCase, Message, set_config + + +class DocstringTest(CheckerTestCase): + CHECKER_CLASS = base.DocStringChecker + + 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 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 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) + + @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) + + @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 test_class_no_docstring(self): + klass = test_utils.extract_node(""" + class Klass(object): + pass""") + with self.assertAddsMessages(Message('missing-docstring', node=klass, args=('class',))): + self.checker.visit_class(klass) + + +class NameCheckerTest(CheckerTestCase): + CHECKER_CLASS = base.NameChecker + CONFIG = { + 'bad_names': set(), + } + + @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. + methods = test_utils.extract_node(""" + import abc + + class FooClass(object): + @property + def FOO(self): #@ + pass + + @property + def bar(self): #@ + pass + + @abc.abstractproperty + def BAZ(self): #@ + pass + """) + with self.assertNoMessages(): + self.checker.visit_function(methods[0]) + self.checker.visit_function(methods[2]) + with self.assertAddsMessages(Message('invalid-name', node=methods[1], + args=('attribute', 'bar', ''))): + self.checker.visit_function(methods[1]) + + @set_config(attr_rgx=re.compile('[A-Z]+')) + def test_property_setters(self): + method = test_utils.extract_node(""" + class FooClass(object): + @property + def foo(self): pass + + @foo.setter + def FOOSETTER(self): #@ + pass + """) + with self.assertNoMessages(): + self.checker.visit_function(method) + + def test_module_level_names(self): + assign = test_utils.extract_node(""" + import collections + Class = collections.namedtuple("a", ("b", "c")) #@ + """) + with self.assertNoMessages(): + self.checker.visit_assname(assign.targets[0]) + + assign = test_utils.extract_node(""" + class ClassA(object): + pass + ClassB = ClassA + """) + with self.assertNoMessages(): + self.checker.visit_assname(assign.targets[0]) + + module = test_utils.build_module(""" + def A(): + return 1, 2, 3 + CONSTA, CONSTB, CONSTC = A() + CONSTD = A()""") + with self.assertNoMessages(): + self.checker.visit_assname(module.body[1].targets[0].elts[0]) + self.checker.visit_assname(module.body[2].targets[0]) + + assign = test_utils.extract_node(""" + CONST = "12 34 ".rstrip().split()""") + with self.assertNoMessages(): + self.checker.visit_assname(assign.targets[0]) + + +class MultiNamingStyleTest(CheckerTestCase): + CHECKER_CLASS = base.NameChecker + + MULTI_STYLE_RE = re.compile('(?:(?P[A-Z]+)|(?P[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('(?:(?PFOO)|(?P[A-Z]+)|(?P[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/unittest_checker_format.py b/test/unittest_checker_format.py new file mode 100644 index 0000000..8f1be78 --- /dev/null +++ b/test/unittest_checker_format.py @@ -0,0 +1,257 @@ +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 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 + +Check format checker helper functions +""" + +import sys +import re +from os import linesep +import tokenize +import StringIO + +from logilab.common.testlib import TestCase, unittest_main +from astroid import test_utils + +from pylint.checkers.format import * + +from pylint.testutils import CheckerTestCase, Message, set_config + + +def tokenize_str(code): + return list(tokenize.generate_tokens(StringIO.StringIO(code).readline)) + + +class MultiStatementLineTest(CheckerTestCase): + CHECKER_CLASS = FormatChecker + + def testSingleLineIfStmts(self): + stmt = test_utils.extract_node(""" + if True: pass #@ + """) + with self.assertAddsMessages(Message('multiple-statements', node=stmt.body[0])): + self.checker.process_tokens([]) + self.checker.visit_default(stmt.body[0]) + self.checker.config.single_line_if_stmt = True + with self.assertNoMessages(): + self.checker.process_tokens([]) + self.checker.visit_default(stmt.body[0]) + stmt = test_utils.extract_node(""" + if True: pass #@ + else: + pass + """) + with self.assertAddsMessages(Message('multiple-statements', node=stmt.body[0])): + self.checker.process_tokens([]) + self.checker.visit_default(stmt.body[0]) + + def testTryExceptFinallyNoMultipleStatement(self): + tree = test_utils.extract_node(""" + try: #@ + pass + except: + pass + finally: + pass""") + with self.assertNoMessages(): + self.checker.process_tokens([]) + self.checker.visit_default(tree.body[0]) + + + +class SuperfluousParenthesesTest(CheckerTestCase): + CHECKER_CLASS = FormatChecker + + def testCheckKeywordParensHandlesValidCases(self): + self.checker._keywords_with_parens = set() + cases = [ + 'if foo:', + 'if foo():', + 'if (x and y) or z:', + 'assert foo()', + 'assert ()', + 'if (1, 2) in (3, 4):', + 'if (a or b) in c:', + 'return (x for x in x)', + 'if (x for x in x):', + 'for x in (x for x in x):', + 'not (foo or bar)', + 'not (foo or bar) and baz', + ] + with self.assertNoMessages(): + for code in cases: + self.checker._check_keyword_parentheses(tokenize_str(code), 0) + + def testCheckKeywordParensHandlesUnnecessaryParens(self): + self.checker._keywords_with_parens = set() + cases = [ + (Message('superfluous-parens', line=1, args='if'), + 'if (foo):', 0), + (Message('superfluous-parens', line=1, args='if'), + 'if ((foo, bar)):', 0), + (Message('superfluous-parens', line=1, args='if'), + 'if (foo(bar)):', 0), + (Message('superfluous-parens', line=1, args='return'), + 'return ((x for x in x))', 0), + (Message('superfluous-parens', line=1, args='not'), + 'not (foo)', 0), + (Message('superfluous-parens', line=1, args='not'), + 'if not (foo):', 1), + (Message('superfluous-parens', line=1, args='if'), + 'if (not (foo)):', 0), + (Message('superfluous-parens', line=1, args='not'), + 'if (not (foo)):', 2), + ] + for msg, code, offset in cases: + with self.assertAddsMessages(msg): + self.checker._check_keyword_parentheses(tokenize_str(code), offset) + + def testFuturePrintStatementWithoutParensWarning(self): + code = """from __future__ import print_function +print('Hello world!') +""" + tree = test_utils.build_module(code) + with self.assertNoMessages(): + self.checker.process_module(tree) + self.checker.process_tokens(tokenize_str(code)) + + +class CheckSpaceTest(CheckerTestCase): + CHECKER_CLASS = FormatChecker + + def testParenthesesGood(self): + good_cases = [ + '(a)\n', + '(a * (b + c))\n', + '( #\na)\n', + ] + with self.assertNoMessages(): + for code in good_cases: + self.checker.process_tokens(tokenize_str(code)) + + def testParenthesesBad(self): + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'after', 'bracket', '( a)\n^'))): + self.checker.process_tokens(tokenize_str('( a)\n')) + + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'before', 'bracket', '(a )\n ^'))): + self.checker.process_tokens(tokenize_str('(a )\n')) + + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'before', 'bracket', 'foo (a)\n ^'))): + self.checker.process_tokens(tokenize_str('foo (a)\n')) + + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'before', 'bracket', '{1: 2} [1]\n ^'))): + self.checker.process_tokens(tokenize_str('{1: 2} [1]\n')) + + def testTrailingCommaGood(self): + with self.assertNoMessages(): + self.checker.process_tokens(tokenize_str('(a, )\n')) + self.checker.process_tokens(tokenize_str('(a,)\n')) + + self.checker.config.no_space_check = [] + with self.assertNoMessages(): + self.checker.process_tokens(tokenize_str('(a,)\n')) + + @set_config(no_space_check=[]) + def testTrailingCommaBad(self): + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'before', 'bracket', '(a, )\n ^'))): + self.checker.process_tokens(tokenize_str('(a, )\n')) + + def testComma(self): + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'before', 'comma', '(a , b)\n ^'))): + self.checker.process_tokens(tokenize_str('(a , b)\n')) + + def testSpacesAllowedInsideSlices(self): + good_cases = [ + '[a:b]\n', + '[a : b]\n', + '[a : ]\n', + '[:a]\n', + '[:]\n', + '[::]\n', + ] + with self.assertNoMessages(): + for code in good_cases: + self.checker.process_tokens(tokenize_str(code)) + + def testKeywordSpacingGood(self): + with self.assertNoMessages(): + self.checker.process_tokens(tokenize_str('foo(foo=bar)\n')) + self.checker.process_tokens(tokenize_str('lambda x=1: x\n')) + + def testKeywordSpacingBad(self): + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'before', 'keyword argument assignment', + '(foo =bar)\n ^'))): + self.checker.process_tokens(tokenize_str('(foo =bar)\n')) + + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'after', 'keyword argument assignment', + '(foo= bar)\n ^'))): + self.checker.process_tokens(tokenize_str('(foo= bar)\n')) + + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('No', 'allowed', 'around', 'keyword argument assignment', + '(foo = bar)\n ^'))): + self.checker.process_tokens(tokenize_str('(foo = bar)\n')) + + def testOperatorSpacingGood(self): + good_cases = [ + 'a = b\n' + 'a < b\n' + 'a\n< b\n', + ] + with self.assertNoMessages(): + for code in good_cases: + self.checker.process_tokens(tokenize_str(code)) + + def testOperatorSpacingBad(self): + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('Exactly one', 'required', 'before', 'comparison', 'a< b\n ^'))): + self.checker.process_tokens(tokenize_str('a< b\n')) + + with self.assertAddsMessages( + Message('bad-whitespace', line=1, + args=('Exactly one', 'required', 'after', 'comparison', 'a = (3, 0): + # erff + os.write(fd, bytes(code, 'ascii')) + else: + os.write(fd, code) + + try: + module = test_utils.build_module(code) + module.file = tmp + yield module + finally: + os.close(fd) + os.remove(tmp) + + +class FixmeTest(CheckerTestCase): + CHECKER_CLASS = misc.EncodingChecker + + def test_fixme(self): + with create_file_backed_module( + """a = 1 + # FIXME """) as module: + with self.assertAddsMessages( + Message(msg_id='fixme', line=2, args=u'FIXME')): + self.checker.process_module(module) + + @set_config(notes=[]) + def test_empty_fixme_regex(self): + with create_file_backed_module( + """a = 1 + # fixme + """) as module: + with self.assertNoMessages(): + self.checker.process_module(module) + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff --git a/test/unittest_checker_similar.py b/test/unittest_checker_similar.py new file mode 100644 index 0000000..d797a5f --- /dev/null +++ b/test/unittest_checker_similar.py @@ -0,0 +1,142 @@ +import sys +from logilab.common.testlib import TestCase, unittest_main + +from cStringIO import StringIO +from os.path import join, dirname, abspath + +from pylint.checkers import similar + +SIMILAR1 = join(dirname(abspath(__file__)), 'input', 'similar1') +SIMILAR2 = join(dirname(abspath(__file__)), 'input', 'similar2') + +class SimilarTC(TestCase): + """test the similar command line utility""" + + def test_ignore_comments(self): + sys.stdout = StringIO() + try: + similar.Run(['--ignore-comments', SIMILAR1, SIMILAR2]) + except SystemExit, ex: + self.assertEqual(ex.code, 0) + output = sys.stdout.getvalue() + else: + self.fail('not system exit') + finally: + sys.stdout = sys.__stdout__ + self.assertMultiLineEqual(output.strip(), (""" +10 similar lines in 2 files +==%s:0 +==%s:0 + import one + from two import two + three + four + five + six + seven + eight + nine + ''' ten +TOTAL lines=44 duplicates=10 percent=22.73 +""" % (SIMILAR1, SIMILAR2)).strip()) + + + def test_ignore_docsrings(self): + sys.stdout = StringIO() + try: + similar.Run(['--ignore-docstrings', SIMILAR1, SIMILAR2]) + except SystemExit, ex: + self.assertEqual(ex.code, 0) + output = sys.stdout.getvalue() + else: + self.fail('not system exit') + finally: + sys.stdout = sys.__stdout__ + self.assertMultiLineEqual(output.strip(), (""" +8 similar lines in 2 files +==%s:6 +==%s:6 + seven + eight + nine + ''' ten + ELEVEN + twelve ''' + thirteen + fourteen + +5 similar lines in 2 files +==%s:0 +==%s:0 + import one + from two import two + three + four + five +TOTAL lines=44 duplicates=13 percent=29.55 +""" % ((SIMILAR1, SIMILAR2) * 2)).strip()) + + + def test_ignore_imports(self): + sys.stdout = StringIO() + try: + similar.Run(['--ignore-imports', SIMILAR1, SIMILAR2]) + except SystemExit, ex: + self.assertEqual(ex.code, 0) + output = sys.stdout.getvalue() + else: + self.fail('not system exit') + finally: + sys.stdout = sys.__stdout__ + self.assertMultiLineEqual(output.strip(), """ +TOTAL lines=44 duplicates=0 percent=0.00 +""".strip()) + + + def test_ignore_nothing(self): + sys.stdout = StringIO() + try: + similar.Run([SIMILAR1, SIMILAR2]) + except SystemExit, ex: + self.assertEqual(ex.code, 0) + output = sys.stdout.getvalue() + else: + self.fail('not system exit') + finally: + sys.stdout = sys.__stdout__ + self.assertMultiLineEqual(output.strip(), (""" +5 similar lines in 2 files +==%s:0 +==%s:0 + import one + from two import two + three + four + five +TOTAL lines=44 duplicates=5 percent=11.36 +""" % (SIMILAR1, SIMILAR2)).strip()) + + def test_help(self): + sys.stdout = StringIO() + try: + similar.Run(['--help']) + except SystemExit, ex: + self.assertEqual(ex.code, 0) + else: + self.fail('not system exit') + finally: + sys.stdout = sys.__stdout__ + + def test_no_args(self): + sys.stdout = StringIO() + try: + similar.Run([]) + except SystemExit, ex: + self.assertEqual(ex.code, 1) + else: + self.fail('not system exit') + finally: + sys.stdout = sys.__stdout__ + +if __name__ == '__main__': + unittest_main() diff --git a/test/unittest_checker_typecheck.py b/test/unittest_checker_typecheck.py new file mode 100644 index 0000000..352b6f9 --- /dev/null +++ b/test/unittest_checker_typecheck.py @@ -0,0 +1,42 @@ +"""Unittest for the type checker.""" + +from astroid import test_utils +from pylint.checkers import typecheck +from pylint.testutils import CheckerTestCase, Message, set_config + +class TypeCheckerTest(CheckerTestCase): + "Tests for pylint.checkers.typecheck" + CHECKER_CLASS = typecheck.TypeChecker + + def test_no_member_in_getattr(self): + """Make sure that a module attribute access is checked by pylint. + """ + + node = test_utils.extract_node(""" + import argparse + argparse.THIS_does_not_EXIST + """) + with self.assertAddsMessages( + Message( + 'no-member', + node=node, + args=('Module', 'argparse', 'THIS_does_not_EXIST'))): + self.checker.visit_getattr(node) + + @set_config(ignored_modules=('argparse',)) + def test_no_member_in_getattr_ignored(self): + """Make sure that a module attribute access check is omitted with a + module that is configured to be ignored. + """ + + node = test_utils.extract_node(""" + import argparse + argparse.THIS_does_not_EXIST + """) + with self.assertNoMessages(): + self.checker.visit_getattr(node) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff --git a/test/unittest_checker_variables.py b/test/unittest_checker_variables.py index 6b9b654..30bc5e6 100644 --- a/test/unittest_checker_variables.py +++ b/test/unittest_checker_variables.py @@ -1,8 +1,10 @@ """Unit tests for the variables checker.""" +import sys +import os from astroid import test_utils from pylint.checkers import variables -from pylint.testutils import CheckerTestCase +from pylint.testutils import CheckerTestCase, linter class VariablesCheckerTC(CheckerTestCase): @@ -19,3 +21,23 @@ class VariablesCheckerTC(CheckerTestCase): """) with self.assertNoMessages(): self.walk(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__': + from logilab.common.testlib import unittest_main + unittest_main() diff --git a/test/unittest_utils.py b/test/unittest_utils.py new file mode 100644 index 0000000..d8c4c67 --- /dev/null +++ b/test/unittest_utils.py @@ -0,0 +1,63 @@ +# Copyright 2013 Google Inc. +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +from logilab.common.testlib import TestCase + +from astroid import test_utils +from pylint import utils +from pylint.checkers.utils import check_messages + + +class PyLintASTWalkerTest(TestCase): + class MockLinter(object): + def __init__(self, msgs): + self._msgs = msgs + + def is_message_enabled(self, msgid): + return self._msgs.get(msgid, True) + + class Checker(object): + def __init__(self): + self.called = set() + + @check_messages('first-message') + def visit_module(self, module): + self.called.add('module') + + @check_messages('second-message') + def visit_callfunc(self, module): + raise NotImplementedError + + @check_messages('second-message', 'third-message') + def visit_assname(self, module): + self.called.add('assname') + + @check_messages('second-message') + def leave_assname(self, module): + raise NotImplementedError + + def testCheckMessages(self): + linter = self.MockLinter({'first-message': True, + 'second-message': False, + 'third-message': True}) + walker = utils.PyLintASTWalker(linter) + checker = self.Checker() + walker.add_checker(checker) + walker.walk(test_utils.build_module("x = func()")) + self.assertEqual(set(['module', 'assname']), checker.called) + + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() + -- cgit v1.2.1 From 393973024fa7984cc7efdcb96197c76f32a5e82b Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 18 Apr 2014 14:57:46 +0200 Subject: [packaging] attempt to skip byte-compilation for tests files This doesn't occurs when using bare distutils but does when using setuptools (eg from easy_install/pip). As setuptools seems broken on my box, I've not been able to test that. (Hopefuly) closes #171 --- setup.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 5352833..ea4f28c 100644 --- a/setup.py +++ b/setup.py @@ -132,15 +132,13 @@ class MyInstallLib(install_lib.install_lib): exclude = set() shutil.rmtree(dest, ignore_errors=True) shutil.copytree(directory, dest) - # since python2.5's copytree doesn't support the ignore + # 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) # pylint: disable=no-name-in-module @@ -148,6 +146,14 @@ class MyInstallLib(install_lib.install_lib): print('running 2to3 on', dest) run_2to3([dest]) + # override this since pip/easy_install attempt to byte compile test data + # files, some of them being syntactically wrong by design, and this scares + # the end-user + def byte_compile(self, files): + print 'files', files + testdir = join('pylint', 'test') + files = [f for f in files if testdir not in f] + install_lib.install_lib.byte_compile(self, files) def install(**kwargs): """setup entry point""" -- cgit v1.2.1 From 063eb592dbfa9361f636dc8cb8b54ec6e4360788 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 18 Apr 2014 15:01:32 +0200 Subject: [variable checker] fix dummy-variable-rgx help. Closes #75 --- checkers/variables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checkers/variables.py b/checkers/variables.py index d74cb56..4bc0162 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -170,8 +170,8 @@ __init__ files.'}), ("dummy-variables-rgx", {'default': ('_$|dummy'), 'type' :'regexp', 'metavar' : '', - 'help' : 'A regular expression matching the beginning of \ - the name of dummy variables (i.e. not used).'}), + 'help' : 'A regular expression matching the name of dummy \ +variables (i.e. expectedly not used).'}), ("additional-builtins", {'default': (), 'type' : 'csv', 'metavar' : '', -- cgit v1.2.1 From 3b86291897c0a610a332201fc5f117caf080993b Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 18 Apr 2014 15:28:55 +0200 Subject: oops, drop unintentionnaly commited print, breaking py3 install --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ea4f28c..226b39e 100644 --- a/setup.py +++ b/setup.py @@ -150,7 +150,6 @@ class MyInstallLib(install_lib.install_lib): # files, some of them being syntactically wrong by design, and this scares # the end-user def byte_compile(self, files): - print 'files', files testdir = join('pylint', 'test') files = [f for f in files if testdir not in f] install_lib.install_lib.byte_compile(self, files) -- cgit v1.2.1 From 8ae7a22ad67a416068068850c95f0f98e35e6072 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 18 Apr 2014 15:30:12 +0200 Subject: regenerate doc/example/man --- examples/pylintrc | 133 ++++++++++++++++++++++++++++++++++++++++-------------- man/pylint.1 | 6 ++- 2 files changed, 104 insertions(+), 35 deletions(-) diff --git a/examples/pylintrc b/examples/pylintrc index d560f9e..047b473 100644 --- a/examples/pylintrc +++ b/examples/pylintrc @@ -12,7 +12,7 @@ profile=no # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS +ignore=.hg,test # Pickle collected data for later comparisons. persistent=yes @@ -21,6 +21,12 @@ persistent=yes # usually to register additional checkers. load-plugins= +# DEPRECATED +include-ids=no + +# DEPRECATED +symbols=no + [MESSAGES CONTROL] @@ -48,12 +54,6 @@ load-plugins= # mypackage.mymodule.MyReporterClass. output-format=text -# Include message's id in output -include-ids=no - -# Include symbolic ids of messages in output -symbols=no - # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". @@ -73,12 +73,33 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme # evaluation report (RP0004). comment=no +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + [FORMAT] # Maximum number of characters on a single line. max-line-length=80 +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + # Maximum number of lines in a module max-module-lines=1000 @@ -86,8 +107,6 @@ max-module-lines=1000 # tab). indent-string=' ' -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ [BASIC] @@ -97,44 +116,87 @@ required-attributes= # List of builtins function names that should not be used, separated by a comma bad-functions=map,filter,apply,input -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= -# Regular expression which should only match correct function names +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Regular expression matching correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ -# Regular expression which should only match correct instance attribute names +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ -# Regular expression which should only match correct argument names +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ -# Regular expression which should only match correct list comprehension / -# generator expression variable names +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ -# Regular expression which should only match functions or classes name which do -# not require a docstring +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. no-docstring-rgx=__.*__ +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + [SIMILARITIES] @@ -163,6 +225,11 @@ notes=FIXME,XXX,TODO # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus extisting member attributes cannot be deduced by static analysis +ignored-modules= + # List of classes names for which member attributes should not be checked # (useful for classes with attributes dynamically set). ignored-classes=SQLObject @@ -182,8 +249,8 @@ generated-members=REQUEST,acl_users,aq_parent # Tells whether we should check for unused import in __init__ files. init-import=no -# A regular expression matching the beginning of the name of dummy variables -# (i.e. not used). +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that @@ -225,7 +292,7 @@ max-locals=15 max-returns=6 # Maximum number of branch for function / method body -max-branchs=12 +max-branches=12 # Maximum number of statements in function / method body max-statements=50 diff --git a/man/pylint.1 b/man/pylint.1 index f80c61c..480ac18 100644 --- a/man/pylint.1 +++ b/man/pylint.1 @@ -1,4 +1,4 @@ -.TH pylint 1 "2014-4-11" pylint +.TH pylint 1 "2014-4-18" pylint .SH NAME .B pylint \- python code static checker @@ -210,6 +210,8 @@ List of note tags to take in consideration, separated by a comma. [current: FIXM .SH TYPECHECK .IP "--ignore-mixin-members=" Tells whether missing members accessed in mixin class should be ignored. A mixin class is detected if its name ends with "mixin" (case insensitive). [current: yes] +.IP "--ignored-modules=" +List of module names for which member attributes should not be checked (useful for modules/projects where namespaces are manipulated during runtime and thus extisting member attributes cannot be deduced by static analysis [current: none] .IP "--ignored-classes=" List of classes names for which member attributes should not be checked (useful for classes with attributes dynamically set). [current: SQLObject] .IP "--zope=" @@ -221,7 +223,7 @@ List of members which are set dynamically and missed by pylint inference system, .IP "--init-import=" Tells whether we should check for unused import in __init__ files. [current: no] .IP "--dummy-variables-rgx=" -A regular expression matching the beginning of the name of dummy variables (i.e. not used). [current: _|dummy] +A regular expression matching the name of dummy variables (i.e. expectedly not used). [current: _|dummy] .IP "--additional-builtins=" List of additional names supposed to be defined in builtins. Remember that you should avoid to define new builtins when possible. [current: none] -- cgit v1.2.1 From e3bfb0bd308be0bea85a0cc0b987a179daecb983 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 18 Apr 2014 15:41:38 +0200 Subject: pylint 1.1 --- ChangeLog | 8 ++++++-- __pkginfo__.py | 6 +++--- debian/changelog | 6 ++++++ debian/control | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index 68b980f..7c038cb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,8 +1,7 @@ ChangeLog for Pylint ==================== --- - +2014-04-18 -- 1.2.0 * Pass the current python paths to pylint process when invoked via epylint. Fixes BitBucket issue #133. @@ -94,6 +93,7 @@ ChangeLog for Pylint * Do not attempt to analyze non python file, eg .so file (#122) + 2013-12-22 -- 1.1.0 * Add new check for use of deprecated pragma directives "pylint:disable-msg" or "pylint:enable-msg" (I0022, deprecated-pragma) which was previously @@ -153,6 +153,7 @@ ChangeLog for Pylint * Fix issue #55 (false-positive trailing-whitespace on Windows) + 2013-08-06 -- 1.0.0 * Add check for the use of 'exec' function @@ -1399,3 +1400,6 @@ ChangeLog for Pylint 2003-05-19 -- 0.1 * initial release + + + diff --git a/__pkginfo__.py b/__pkginfo__.py index 0aa48a9..ea67fe6 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -19,14 +19,14 @@ import sys modname = distname = 'pylint' -numversion = (1, 1, 0) +numversion = (1, 2, 0) version = '.'.join([str(num) for num in numversion]) if sys.version_info < (2, 6): - install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.0.1', + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1', 'StringFormat'] else: - install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.0.1'] + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1'] license = 'GPL' description = "python code static checker" diff --git a/debian/changelog b/debian/changelog index f9d42f2..c52023c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +pylint (1.2.0-0) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Fri, 18 Apr 2014 15:32:32 +0200 + pylint (1.1.0-1) unstable; urgency=low * new upstream release diff --git a/debian/control b/debian/control index 313445a..442737d 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 (>= 1.0.1) + python-astroid (>= 1.1) Suggests: python-tk XB-Python-Version: ${python:Versions} Description: python code static checker and UML diagram generator -- cgit v1.2.1 -- cgit v1.2.1 From 75e8a83370ea8ab732d731279f8c92b4e6fc7a4f Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 18 Apr 2014 17:46:48 +0200 Subject: Handle <> in the surface token dispatcher part. --- checkers/format.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/checkers/format.py b/checkers/format.py index c957816..fed9e29 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -658,6 +658,10 @@ class FormatChecker(BaseTokenChecker): def _inside_brackets(self, left): return self._bracket_stack[-1] == left + def _handle_old_ne_operator(self, tokens, i): + if tokens[i][1] == '<>': + self.add_message('old-ne-operator', line=tokens[i][2][0]) + def _prepare_token_dispatcher(self): raw = [ (_KEYWORD_TOKENS, @@ -676,6 +680,8 @@ class FormatChecker(BaseTokenChecker): ([':'], self._handle_colon), (['lambda'], self._open_lambda), + + (['<>'], self._handle_old_ne_operator), ] dispatch = {} @@ -728,13 +734,10 @@ class FormatChecker(BaseTokenChecker): if tok_type not in indent_junk: previous = tok_type, token, start[0] self._current_line.handle_line_start(idx) - if tok_type == tokenize.OP: - if token == '<>': - self.add_message('old-ne-operator', line=line_num) - elif tok_type == tokenize.NUMBER: + + if tok_type == tokenize.NUMBER: if token.endswith('l'): self.add_message('lowercase-l-suffix', line=line_num) - elif tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form @@ -742,7 +745,6 @@ class FormatChecker(BaseTokenChecker): # If an INDENT appears, setting check_equal is wrong, and will # be undone when we see the INDENT. check_equal = True - self._process_retained_warnings(TokenWrapper(tokens), idx) self._current_line.next_logical_line() elif tok_type == tokenize.INDENT: -- cgit v1.2.1 From e81071c1ccaecb9dc0c869c70733820f6b5115c9 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 18 Apr 2014 18:32:55 +0200 Subject: Simplify the processing inside the main token loop, now that we have access to the full token stream. Also fix a bug with misindented lines that start with a number not being reported properly. --- checkers/format.py | 64 ++++++++++++++++++++----------------------- test/input/func_indent.py | 1 + test/messages/func_indent.txt | 2 ++ 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/checkers/format.py b/checkers/format.py index fed9e29..c02986e 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -49,6 +49,7 @@ _CLOSING_BRACKETS = [')', ']', '}'] _TAB_LENGTH = 8 _EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT]) +_JUNK_TOKENS = (tokenize.COMMENT, tokenize.NL) # Whitespace checking policy constants _MUST = 0 @@ -143,6 +144,13 @@ def _column_distance(token1, token2): return token2[2][1] - token1[3][1] +def _last_token_on_line_is(tokens, line_end, token): + return ( + line_end > 0 and tokens.token(line_end-1) == token or + line_end > 1 and tokens.token(line_end-2) == token + and tokens.type(line_end-1) == tokenize.COMMENT) + + def _token_followed_by_eol(tokens, position): return (tokens.type(position+1) == tokenize.NL or tokens.type(position+1) == tokenize.COMMENT and @@ -453,9 +461,14 @@ class FormatChecker(BaseTokenChecker): self._bracket_stack.append(token) self._current_line.push_token(token, idx) - def new_line(self, tok_type, line, line_num, junk): + def new_line(self, tokens, line_end, line_start): """a new line has been encountered, process it if necessary""" - if not tok_type in junk: + if _last_token_on_line_is(tokens, line_end, ';'): + self.add_message('unnecessary-semicolon', line=tokens.start_line(line_end)) + + line_num = tokens.start_line(line_start) + line = tokens.line(line_start) + if tokens.type(line_start) not in _JUNK_TOKENS: self._lines[line_num] = line.split('\n')[0] self.check_lines(line, line_num) @@ -700,45 +713,26 @@ class FormatChecker(BaseTokenChecker): regular expression). """ self._bracket_stack = [None] - # TODO(tmarek): extra to toplevel - junk = (tokenize.COMMENT, tokenize.NL) - indent_junk = set(junk) | set([tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT]) - indents = [0] - check_equal = 0 + check_equal = False line_num = 0 - previous = None self._lines = {} self._visited_lines = {} - new_line_delay = False token_handlers = self._prepare_token_dispatcher() self._current_line = ContinuedLineState(tokens, self.config) for idx, (tok_type, token, start, _, line) in enumerate(tokens): - if new_line_delay: - new_line_delay = False - self.new_line(tok_type, line, line_num, junk) if start[0] != line_num: - if previous is not None and previous[0] == tokenize.OP and previous[1] == ';': - self.add_message('unnecessary-semicolon', line=previous[2]) - previous = None line_num = start[0] # A tokenizer oddity: if an indented line contains a multi-line # docstring, the line member of the INDENT token does not contain - # the full line; therefore we delay checking the new line until - # the next token. + # the full line; therefore we check the next token on the line. if tok_type == tokenize.INDENT: - new_line_delay = True + self.new_line(TokenWrapper(tokens), idx-1, idx+1) else: - self.new_line(tok_type, line, line_num, junk) - if tok_type not in indent_junk: - previous = tok_type, token, start[0] - self._current_line.handle_line_start(idx) + self.new_line(TokenWrapper(tokens), idx-1, idx) - if tok_type == tokenize.NUMBER: - if token.endswith('l'): - self.add_message('lowercase-l-suffix', line=line_num) - elif tok_type == tokenize.NEWLINE: + if tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form # (NL | COMMENT)* (INDENT | DEDENT+)? @@ -762,15 +756,20 @@ class FormatChecker(BaseTokenChecker): elif tok_type == tokenize.NL: self._check_continued_indentation(TokenWrapper(tokens), idx+1) self._current_line.next_physical_line() - elif check_equal and tok_type not in junk: - # this is the first "real token" following a NEWLINE, so it + elif tok_type != tokenize.COMMENT: + self._current_line.handle_line_start(idx) + # This is the first concrete token following a NEWLINE, so it # must be the first token of the next program statement, or an # ENDMARKER; the "line" argument exposes the leading whitespace # for this statement; in the case of ENDMARKER, line is an empty # string, so will properly match the empty string with which the # "indents" stack was seeded - check_equal = False - self.check_indent_level(line, indents[-1], line_num) + if check_equal: + check_equal = False + self.check_indent_level(line, indents[-1], line_num) + + if tok_type == tokenize.NUMBER and token.endswith('l'): + self.add_message('lowercase-l-suffix', line=line_num) try: handler = token_handlers[token] @@ -784,10 +783,7 @@ class FormatChecker(BaseTokenChecker): self.add_message('too-many-lines', args=line_num, line=1) def _process_retained_warnings(self, tokens, current_pos): - single_line_block_stmt = not ( - tokens.token(current_pos-1) == ':' or - current_pos > 1 and tokens.token(current_pos-2) == ':' - and tokens.type(current_pos-1) == tokenize.COMMENT) + single_line_block_stmt = not _last_token_on_line_is(tokens, current_pos, ':') for indent_pos, state, offsets in self._current_line.retained_warnings: block_type = offsets[tokens.start_col(indent_pos)] diff --git a/test/input/func_indent.py b/test/input/func_indent.py index 18159e9..3c6ee41 100644 --- a/test/input/func_indent.py +++ b/test/input/func_indent.py @@ -11,6 +11,7 @@ def tutuu(): def titii(): """also malindented""" + 1 # and this. def tataa(kdict): """blank line unindented""" diff --git a/test/messages/func_indent.txt b/test/messages/func_indent.txt index aa3645f..e24af18 100644 --- a/test/messages/func_indent.txt +++ b/test/messages/func_indent.txt @@ -1,3 +1,5 @@ W: 5: Bad indentation. Found 1 spaces, expected 4 W: 6: Bad indentation. Found 1 spaces, expected 4 W: 13: Bad indentation. Found 5 spaces, expected 4 +W: 14: Bad indentation. Found 5 spaces, expected 4 +W: 14:titii: Statement seems to have no effect -- cgit v1.2.1 From 6954a8cb93af517039c3a11dd0f8a5829a22977d Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 18 Apr 2014 19:09:35 +0200 Subject: * Emit [assignment-from-none] when the function contains bare returns. Fixes issue #191. --- ChangeLog | 3 +++ checkers/typecheck.py | 3 ++- test/input/func_typecheck_callfunc_assigment.py | 7 +++++++ test/messages/func_typecheck_callfunc_assigment.txt | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 679a7ba..3002eeb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,9 @@ ChangeLog for Pylint * Add a new warning [bad-continuation] for badly indentend continued lines. + * Emit [assignment-from-none] when the function contains bare returns. + Fixes BitBucket issue #191. + * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 5fe293d..4cb406f 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -287,7 +287,8 @@ accessed. Python regular expressions are accepted.'} else: for rnode in returns: if not (isinstance(rnode.value, astroid.Const) - and rnode.value.value is None): + and rnode.value.value is None + or rnode.value is None): break else: self.add_message('assignment-from-none', node=node) diff --git a/test/input/func_typecheck_callfunc_assigment.py b/test/input/func_typecheck_callfunc_assigment.py index a909f17..838b27c 100644 --- a/test/input/func_typecheck_callfunc_assigment.py +++ b/test/input/func_typecheck_callfunc_assigment.py @@ -28,6 +28,13 @@ def func_return_none(): A = func_return_none() +def func_implicit_return_none(): + """Function returning None from bare return statement.""" + return + +A = func_implicit_return_none() + + def func_return_none_and_smth(): """function returning none and something else""" print 'dougloup' diff --git a/test/messages/func_typecheck_callfunc_assigment.txt b/test/messages/func_typecheck_callfunc_assigment.txt index 96ad43e..1d84510 100644 --- a/test/messages/func_typecheck_callfunc_assigment.txt +++ b/test/messages/func_typecheck_callfunc_assigment.txt @@ -1,2 +1,3 @@ E: 20: Assigning to function call which doesn't return W: 28: Assigning to function call which only returns None +W: 35: Assigning to function call which only returns None -- cgit v1.2.1 From 7293b44ca5f69239d6806fee223b13273ed401c7 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 22 Apr 2014 11:10:27 +0200 Subject: fix debian changelog --- debian/changelog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index c52023c..fd43e38 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -pylint (1.2.0-0) unstable; urgency=low +pylint (1.2.0-1) unstable; urgency=low * new upstream release -- cgit v1.2.1 -- cgit v1.2.1 -- cgit v1.2.1 From 8f5c493a852bd78d1678c75057250c0fa8a8a6a2 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 22 Apr 2014 11:18:25 +0200 Subject: [doc]?use symbolic name in message control examlple. Closes #27 --- doc/message-control.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/message-control.rst b/doc/message-control.rst index d93b452..1929d73 100644 --- a/doc/message-control.rst +++ b/doc/message-control.rst @@ -22,34 +22,34 @@ An example available from the examples directory: def meth2(self, arg): """and this one not""" - # pylint: disable=W0613 + # pylint: disable=unused-argument print self\ + "foo" def meth3(self): """test one line disabling""" # no error - print self.bla # pylint: disable=E1101 + print self.bla # pylint: disable=no-member # error print self.blop def meth4(self): """test re-enabling""" - # pylint: disable=E1101 + # pylint: disable=no-member # no error print self.bla print self.blop - # pylint: enable=E1101 + # pylint: enable=no-member # error print self.blip def meth5(self): """test IF sub-block re-enabling""" - # pylint: disable=E1101 + # pylint: disable=no-member # no error print self.bla if self.blop: - # pylint: enable=E1101 + # pylint: enable=no-member # error print self.blip else: @@ -60,14 +60,14 @@ An example available from the examples directory: def meth6(self): """test TRY/EXCEPT sub-block re-enabling""" - # pylint: disable=E1101 + # pylint: disable=no-member # no error print self.bla try: - pylint: enable=E1101 + pylint: enable=no-member # error print self.blip - except UndefinedName: # pylint: disable=E0602 + except UndefinedName: # pylint: disable=undefined-variable # no error print self.blip # no error @@ -75,7 +75,7 @@ An example available from the examples directory: def meth7(self): """test one line block opening disabling""" - if self.blop: # pylint: disable=E1101 + if self.blop: # pylint: disable=no-member # error print self.blip else: @@ -89,7 +89,7 @@ An example available from the examples directory: """test late disabling""" # error print self.blip - # pylint: disable=E1101 + # pylint: disable=no-member # no error print self.bla print self.blop -- cgit v1.2.1 From 282ee5be047cddc860d70bb2ac40068cbd994b1b Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 22 Apr 2014 11:27:00 +0200 Subject: ensure doc has the correct version --- doc/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index b4b8fbd..d89693e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,9 @@ copyright = u'2013-2014, Logilab and contributors' # built documents. # # The short X.Y version. -version = '1.1.0' +from pylint.__pkginfo__ import version # The full version, including alpha/beta/rc tags. -release = '1.1.0' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -- cgit v1.2.1 From d49f2971afcbb271fc85496ed8fe8f6b42e7362f Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 22 Apr 2014 20:35:29 +0300 Subject: Fix a potential crash with super inference. --- checkers/newstyle.py | 15 +++++++++++++-- test/input/func_newstyle_super.py | 9 +++++++++ test/messages/func_newstyle_super.txt | 3 ++- test/messages/func_newstyle_super_py30.txt | 1 + 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/checkers/newstyle.py b/checkers/newstyle.py index 8bdbb25..f801c44 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -131,8 +131,19 @@ class NewStyleConflictChecker(BaseChecker): continue if klass is not supcls: - self.add_message('bad-super-call', node=call, - args=(call.args[0].name, )) + name = None + # if supcls is not YES, then supcls was infered + # and use its name. Otherwise, try to look + # for call.args[0].name + if supcls is not astroid.YES: + name = supcls.name + else: + if hasattr(call.args[0], 'name'): + name = call.args[0].name + if name is not None: + self.add_message('bad-super-call', + node=call, + args=(name, )) def register(linter): diff --git a/test/input/func_newstyle_super.py b/test/input/func_newstyle_super.py index a0aaa0a..10e9b28 100644 --- a/test/input/func_newstyle_super.py +++ b/test/input/func_newstyle_super.py @@ -36,3 +36,12 @@ class WrongNameRegression(Py3kAaaa): """ test a regression with the message """ def __init__(self): super(Missing, self).__init__() + +class Getattr(object): + """ crash """ + name = NewAaaa + +class CrashSuper(object): + """ test a crash with this checker """ + def __init__(self): + super(Getattr.name, self).__init__() diff --git a/test/messages/func_newstyle_super.txt b/test/messages/func_newstyle_super.txt index b51869b..ce71293 100644 --- a/test/messages/func_newstyle_super.txt +++ b/test/messages/func_newstyle_super.txt @@ -4,4 +4,5 @@ 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 +E: 38:WrongNameRegression.__init__: Bad first argument 'Missing' given to super() +E: 47:CrashSuper.__init__: Bad first argument 'NewAaaa' 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 b1ee6cd..ecabca5 100644 --- a/test/messages/func_newstyle_super_py30.txt +++ b/test/messages/func_newstyle_super_py30.txt @@ -1,3 +1,4 @@ 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() +E: 47:CrashSuper.__init__: Bad first argument 'NewAaaa' given to super() \ No newline at end of file -- cgit v1.2.1 From 41ce7ee12f464ba0c2a5967a43e1a1a93f1081c6 Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 22 Apr 2014 20:49:52 +0300 Subject: Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. --- ChangeLog | 5 +++++ checkers/variables.py | 4 ---- test/input/func_unbalanced_tuple_unpacking.py | 9 +++++++++ test/input/func_unpacking_non_sequence.py | 20 +++++++++++++++++++- test/messages/func_unbalanced_tuple_unpacking.txt | 2 ++ test/messages/func_unpacking_non_sequence.txt | 5 ++++- 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7c038cb..8bb3ba9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,11 @@ ChangeLog for Pylint ==================== +-- + + * Extend the checking for unbalanced-tuple-unpacking and + unpacking-non-sequence to instance attribute unpacking as well. + 2014-04-18 -- 1.2.0 * Pass the current python paths to pylint process when invoked via epylint. Fixes BitBucket issue #133. diff --git a/checkers/variables.py b/checkers/variables.py index 4bc0162..2ce7fcf 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -605,10 +605,6 @@ builtins. Remember that you should avoid to define new builtins when possible.' return targets = node.targets[0].itered() - if any(not isinstance(target_node, astroid.AssName) - for target_node in targets): - return - try: for infered in node.value.infer(): self._check_unpacking(infered, node, targets) diff --git a/test/input/func_unbalanced_tuple_unpacking.py b/test/input/func_unbalanced_tuple_unpacking.py index c2e01e9..216d520 100644 --- a/test/input/func_unbalanced_tuple_unpacking.py +++ b/test/input/func_unbalanced_tuple_unpacking.py @@ -68,3 +68,12 @@ def do_stuff9(): """ This is not correct """ first, second = unpack() return first + second + +class UnbalancedUnpacking(object): + """ Test unbalanced tuple unpacking in instance attributes. """ + # pylint: disable=attribute-defined-outside-init, invalid-name, too-few-public-methods + def test(self): + """ unpacking in instance attributes """ + self.a, self.b = temp() + self.a, self.b = temp2() + self.a, self.b = unpack() diff --git a/test/input/func_unpacking_non_sequence.py b/test/input/func_unpacking_non_sequence.py index 469add7..6fee0bf 100644 --- a/test/input/func_unpacking_non_sequence.py +++ b/test/input/func_unpacking_non_sequence.py @@ -1,6 +1,6 @@ """Check unpacking non-sequences in assignments. """ -# pylint: disable=too-few-public-methods, invalid-name +# pylint: disable=too-few-public-methods, invalid-name, attribute-defined-outside-init, unused-variable from os import rename as nonseq_func from input.unpacking import nonseq @@ -66,3 +66,21 @@ a, b = nonseq a, b = nonseq() a, b = bad_unpacking() a, b = nonseq_func + +class ClassUnpacking(object): + """ Check unpacking as instance attributes. """ + + def test(self): + """ test unpacking in instance attributes. """ + + self.a, self.b = 1, 2 + self.a, self.b = {1: 2, 2: 3} + self.a, self.b = "xy" + self.a, c = "xy" + c, self.a = good_unpacking() + self.a, self.b = Iter() + + self.a, self.b = NonSeq() + self.a, self.b = ValueError + self.a, self.b = bad_unpacking() + self.a, c = nonseq_func diff --git a/test/messages/func_unbalanced_tuple_unpacking.txt b/test/messages/func_unbalanced_tuple_unpacking.txt index c9311cc..59031e4 100644 --- a/test/messages/func_unbalanced_tuple_unpacking.txt +++ b/test/messages/func_unbalanced_tuple_unpacking.txt @@ -3,4 +3,6 @@ W: 14:do_stuff1: Possible unbalanced tuple unpacking with sequence [1, 2, 3]: le W: 19:do_stuff2: Possible unbalanced tuple unpacking with sequence (1, 2, 3): left side has 2 label(s), right side has 3 value(s) W: 50:do_stuff7: Possible unbalanced tuple unpacking with sequence defined at line 46: left side has 2 label(s), right side has 3 value(s) W: 69:do_stuff9: Possible unbalanced tuple unpacking with sequence defined at line 7 of input.unpacking: left side has 2 label(s), right side has 3 value(s) +W: 77:UnbalancedUnpacking.test: Possible unbalanced tuple unpacking with sequence defined at line 46: left side has 2 label(s), right side has 3 value(s) +W: 79:UnbalancedUnpacking.test: Possible unbalanced tuple unpacking with sequence defined at line 7 of input.unpacking: left side has 2 label(s), right side has 3 value(s) diff --git a/test/messages/func_unpacking_non_sequence.txt b/test/messages/func_unpacking_non_sequence.txt index 4099e29..7386f10 100644 --- a/test/messages/func_unpacking_non_sequence.txt +++ b/test/messages/func_unpacking_non_sequence.txt @@ -6,4 +6,7 @@ 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 - +W: 83:ClassUnpacking.test: Attempting to unpack a non-sequence defined at line 52 +W: 84:ClassUnpacking.test: Attempting to unpack a non-sequence +W: 85:ClassUnpacking.test: Attempting to unpack a non-sequence defined at line 58 +W: 86:ClassUnpacking.test: Attempting to unpack a non-sequence -- cgit v1.2.1 From 535b6eab04ed6bf40e8c7e1fa37880888254bc6d Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 22 Apr 2014 23:20:53 +0300 Subject: Add 'indexing-exception' warning, which detects that indexing an exception occurs in Python 2 (behaviour removed in Python 3) --- ChangeLog | 3 +++ checkers/exceptions.py | 16 ++++++++++++++++ test/input/func_indexing_exceptions_py_30.py | 16 ++++++++++++++++ test/messages/func_indexing_exceptions_py_30.txt | 4 ++++ 4 files changed, 39 insertions(+) create mode 100644 test/input/func_indexing_exceptions_py_30.py create mode 100644 test/messages/func_indexing_exceptions_py_30.txt diff --git a/ChangeLog b/ChangeLog index 8bb3ba9..e40459d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,9 @@ ChangeLog for Pylint * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. + * Add 'indexing-exception' warning, which detects that indexing + an exception occurs in Python 2 (behaviour removed in Python 3). + 2014-04-18 -- 1.2.0 * Pass the current python paths to pylint process when invoked via epylint. Fixes BitBucket issue #133. diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 64aca5b..84f92ea 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -107,6 +107,11 @@ MSGS = { 'clauses. ' 'See http://www.python.org/dev/peps/pep-3110/', {'maxversion': (3, 0)}), + 'W0713': ('Indexing exceptions will not work on Python 3', + 'indexing-exception', + 'Indexing exceptions will not work on Python 3. Use ' + '`exception.args[index]` instead.', + {'maxversion': (3, 0)}), } @@ -211,6 +216,17 @@ class ExceptionsChecker(BaseChecker): if isinstance(node.name, (astroid.Tuple, astroid.List)): self.add_message('unpacking-in-except', node=node) + @check_messages('indexing-exception') + def visit_subscript(self, node): + """ Look for indexing exceptions. """ + try: + for infered in node.value.infer(): + if not isinstance(infered, astroid.Instance): + continue + if inherit_from_std_ex(infered): + self.add_message('indexing-exception', node=node) + except astroid.InferenceError: + return @check_messages('bare-except', 'broad-except', 'pointless-except', 'binary-op-exception', 'bad-except-order', diff --git a/test/input/func_indexing_exceptions_py_30.py b/test/input/func_indexing_exceptions_py_30.py new file mode 100644 index 0000000..1200631 --- /dev/null +++ b/test/input/func_indexing_exceptions_py_30.py @@ -0,0 +1,16 @@ +""" +Check for indexing exceptions. +""" +# pylint: disable=import-error +__revision__ = 0 +import socket +from unknown import ExtensionException + +class SubException(IndexError): + """ empty """ + +_ = IndexError("test")[0] +_ = ZeroDivisionError("error")[0] +_ = ExtensionException("error")[0] +_ = SubException("error")[1] +_ = socker.error("socket")[0] diff --git a/test/messages/func_indexing_exceptions_py_30.txt b/test/messages/func_indexing_exceptions_py_30.txt new file mode 100644 index 0000000..1ec5412 --- /dev/null +++ b/test/messages/func_indexing_exceptions_py_30.txt @@ -0,0 +1,4 @@ +W: 13: Indexing exceptions will not work on Python 3 +W: 14: Indexing exceptions will not work on Python 3 +W: 16: Indexing exceptions will not work on Python 3 +W: 17: Indexing exceptions will not work on Python 3 \ No newline at end of file -- cgit v1.2.1 From 1ba4e5bf3fdc312c06432c4e08e38fba7243565c Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 22 Apr 2014 23:23:34 +0300 Subject: Fix name. --- test/input/func_indexing_exceptions_py_30.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/input/func_indexing_exceptions_py_30.py b/test/input/func_indexing_exceptions_py_30.py index 1200631..95efb6c 100644 --- a/test/input/func_indexing_exceptions_py_30.py +++ b/test/input/func_indexing_exceptions_py_30.py @@ -13,4 +13,4 @@ _ = IndexError("test")[0] _ = ZeroDivisionError("error")[0] _ = ExtensionException("error")[0] _ = SubException("error")[1] -_ = socker.error("socket")[0] +_ = socket.error("socket")[0] -- cgit v1.2.1 From 5d0874d07d58716158efae4bead9cbe18bd15c9c Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 22 Apr 2014 23:27:05 +0300 Subject: Fix the test file. --- test/messages/func_indexing_exceptions_py_30.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/messages/func_indexing_exceptions_py_30.txt b/test/messages/func_indexing_exceptions_py_30.txt index 1ec5412..9c86ba5 100644 --- a/test/messages/func_indexing_exceptions_py_30.txt +++ b/test/messages/func_indexing_exceptions_py_30.txt @@ -1,4 +1,3 @@ +W: 12: Indexing exceptions will not work on Python 3 W: 13: Indexing exceptions will not work on Python 3 -W: 14: Indexing exceptions will not work on Python 3 -W: 16: Indexing exceptions will not work on Python 3 -W: 17: Indexing exceptions will not work on Python 3 \ No newline at end of file +W: 15: Indexing exceptions will not work on Python 3 \ No newline at end of file -- cgit v1.2.1 From 5a130703e1a1f23bc5f80294527299675de1293e Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Wed, 23 Apr 2014 20:17:19 +0200 Subject: Added a new warning for closing over variables that are defined in loops. --- ChangeLog | 3 + checkers/variables.py | 28 +++++ test/input/func_loopvar_in_closure.py | 114 +++++++++++++++++++++ test/input/func_loopvar_in_dict_comp_py27.py | 8 ++ .../func_break_or_return_in_try_finally.txt | 1 + test/messages/func_loopvar_in_closure.txt | 8 ++ test/messages/func_loopvar_in_dict_comp_py27.txt | 1 + 7 files changed, 163 insertions(+) create mode 100644 test/input/func_loopvar_in_closure.py create mode 100644 test/input/func_loopvar_in_dict_comp_py27.py create mode 100644 test/messages/func_loopvar_in_closure.txt create mode 100644 test/messages/func_loopvar_in_dict_comp_py27.txt diff --git a/ChangeLog b/ChangeLog index 3002eeb..b5ca9d1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,9 @@ ChangeLog for Pylint * Emit [assignment-from-none] when the function contains bare returns. Fixes BitBucket issue #191. + * Added a new warning for closing over variables that are + defined in loops. Fixes Bitbucket issue #176. + * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. diff --git a/checkers/variables.py b/checkers/variables.py index 2ce7fcf..a6123de 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -146,6 +146,12 @@ MSGS = { 'Used when something which is not ' 'a sequence is used in an unpack assignment'), + 'W0640': ('Cell variable %s defined in loop', + 'cell-var-from-loop', + 'A variable used in a closure is defined in a loop. ' + 'This will result in all closures using the same value for ' + 'the closed-over variable.'), + } class VariablesChecker(BaseChecker): @@ -423,6 +429,25 @@ builtins. Remember that you should avoid to define new builtins when possible.' if default_message: self.add_message('global-statement', node=node) + def _check_late_binding_closure(self, node, assignment_node, scope_type): + node_scope = node.scope() + if not isinstance(node_scope, (astroid.Lambda, astroid.Function)): + return + + if isinstance(assignment_node, astroid.Comprehension): + if assignment_node.parent.parent_of(node.scope()): + self.add_message('cell-var-from-loop', node=node, args=node.name) + else: + assign_scope = assignment_node.scope() + maybe_for = assignment_node + while not isinstance(maybe_for, astroid.For): + if maybe_for is assign_scope: + break + maybe_for = maybe_for.parent + else: + if maybe_for.parent_of(node_scope) and not isinstance(node_scope.statement(), astroid.Return): + self.add_message('cell-var-from-loop', node=node, args=node.name) + def _loopvar_name(self, node, name): # filter variables according to node's scope # XXX used to filter parents but don't remember why, and removing this @@ -509,6 +534,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' # the name has already been consumed, only check it's not a loop # variable used outside the loop if name in consumed: + defnode = assign_parent(consumed[name][0]) + self._check_late_binding_closure(node, defnode, scope_type) self._loopvar_name(node, name) break # mark the name as consumed if it's defined in this scope @@ -520,6 +547,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' # checks for use before assignment defnode = assign_parent(to_consume[name][0]) if defnode is not None: + self._check_late_binding_closure(node, defnode, scope_type) defstmt = defnode.statement() defframe = defstmt.frame() maybee0601 = True diff --git a/test/input/func_loopvar_in_closure.py b/test/input/func_loopvar_in_closure.py new file mode 100644 index 0000000..32b7a6c --- /dev/null +++ b/test/input/func_loopvar_in_closure.py @@ -0,0 +1,114 @@ +"""Tests for loopvar-in-closure.""" + +__revision__ = 0 + + +def good_case(): + """No problems here.""" + lst = [] + for i in range(10): + lst.append(i) + + +def good_case2(): + """No problems here.""" + return [i for i in range(10)] + + +def good_case3(): + """No problems here.""" + lst = [] + for i in range(10): + lst.append(lambda i=i: i) + + +def good_case4(): + """No problems here.""" + lst = [] + for i in range(10): + print i + lst.append(lambda i: i) + + +def good_case5(): + """No problems here.""" + return (i for i in range(10)) + + +def good_case6(): + """Accept use of the variable after the loop. + + There's already a warning about possibly undefined loop variables, and + the value will not change any more.""" + for i in range(10): + print i + return lambda: i + + +def good_case7(): + """Accept use of the variable inside return.""" + for i in range(10): + if i == 8: + return lambda: i + return lambda: -1 + + +def bad_case(): + """Closing over a loop variable.""" + lst = [] + for i in range(10): + print i + lst.append(lambda: i) + + +def bad_case2(): + """Closing over a loop variable.""" + return [lambda: i for i in range(10)] + + +def bad_case3(): + """Closing over variable defined in loop.""" + lst = [] + for i in range(10): + j = i * i + lst.append(lambda: j) + return lst + + +def bad_case4(): + """Closing over variable defined in loop.""" + lst = [] + for i in range(10): + def nested(): + """Nested function.""" + return i**2 + lst.append(nested) + return lst + + +def bad_case5(): + """Problematic case. + + If this function is used as + + >>> [x() for x in bad_case5()] + + it behaves 'as expected', i.e. the result is range(10). + + If it's used with + + >>> lst = list(bad_case5()) + >>> [x() for x in lst] + + the result is [9] * 10 again. + """ + return (lambda: i for i in range(10)) + + +def bad_case6(): + """Closing over variable defined in loop.""" + lst = [] + for i, j in zip(range(10), range(10, 20)): + print j + lst.append(lambda: i) + return lst diff --git a/test/input/func_loopvar_in_dict_comp_py27.py b/test/input/func_loopvar_in_dict_comp_py27.py new file mode 100644 index 0000000..312eee7 --- /dev/null +++ b/test/input/func_loopvar_in_dict_comp_py27.py @@ -0,0 +1,8 @@ +"""Tests for loopvar-in-closure.""" + +__revision__ = 0 + + +def bad_case(): + """Loop variable from dict comprehension.""" + return {x: lambda: x for x in range(10)} diff --git a/test/messages/func_break_or_return_in_try_finally.txt b/test/messages/func_break_or_return_in_try_finally.txt index 4b674ae..04f27fe 100644 --- a/test/messages/func_break_or_return_in_try_finally.txt +++ b/test/messages/func_break_or_return_in_try_finally.txt @@ -1,2 +1,3 @@ W: 18:insidious_break_and_return: break statement in finally block may swallow exception W: 20:insidious_break_and_return: return statement in finally block may swallow exception +W: 39:break_and_return.strange: Cell variable my_var defined in loop diff --git a/test/messages/func_loopvar_in_closure.txt b/test/messages/func_loopvar_in_closure.txt new file mode 100644 index 0000000..6ca613a --- /dev/null +++ b/test/messages/func_loopvar_in_closure.txt @@ -0,0 +1,8 @@ +W: 21:good_case3: Unused variable 'i' +W: 45:good_case6.: Using possibly undefined loop variable 'i' +W: 61:bad_case.: Cell variable i defined in loop +W: 66:bad_case2.: Cell variable i defined in loop +W: 74:bad_case3.: Cell variable j defined in loop +W: 84:bad_case4.nested: Cell variable i defined in loop +W:105:bad_case5.: Cell variable i defined in loop +W:113:bad_case6.: Cell variable i defined in loop diff --git a/test/messages/func_loopvar_in_dict_comp_py27.txt b/test/messages/func_loopvar_in_dict_comp_py27.txt new file mode 100644 index 0000000..bc11121 --- /dev/null +++ b/test/messages/func_loopvar_in_dict_comp_py27.txt @@ -0,0 +1 @@ +W: 8:bad_case.: Cell variable x defined in loop -- cgit v1.2.1 From a1bf229f85aa0285f8a963f46109a06520a60ea7 Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 25 Apr 2014 13:17:17 +0300 Subject: Fix a potential AttributeError when checking for `reversed` arguments. --- checkers/base.py | 13 ++++++++----- test/input/func_bad_reversed_sequence.py | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/checkers/base.py b/checkers/base.py index ac58177..8136d0f 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -785,11 +785,14 @@ functions, methods if argument is None: # nothing was infered # try to see if we have iter() - if (isinstance(node.args[0], astroid.CallFunc) and - node.args[0].func.name == 'iter'): - func = node.args[0].func.infer().next() - if is_builtin_object(func): - self.add_message('bad-reversed-sequence', node=node) + if isinstance(node.args[0], astroid.CallFunc): + try: + func = node.args[0].func.infer().next() + except InferenceError: + return + if (getattr(func, 'name', None) == 'iter' and + is_builtin_object(func)): + self.add_message('bad-reversed-sequence', node=node) return if isinstance(argument, astroid.Instance): diff --git a/test/input/func_bad_reversed_sequence.py b/test/input/func_bad_reversed_sequence.py index a394e53..ad699e6 100644 --- a/test/input/func_bad_reversed_sequence.py +++ b/test/input/func_bad_reversed_sequence.py @@ -37,7 +37,7 @@ def uninferable(seq): """ return reversed(seq) -def test(): +def test(path): """ test function """ seq = reversed() seq = reversed(None) @@ -56,4 +56,5 @@ def test(): seq = reversed(deque([])) seq = reversed("123") seq = uninferable([1, 2, 3]) + seq = reversed(path.split("/")) return seq -- cgit v1.2.1 From f68668644c79777e1ea55effa63b6022c81c839b Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 25 Apr 2014 22:54:52 +0200 Subject: Don't use the print statement in func_bad_continuation to avoid different code in Python 2.x and 3.x --- test/input/func_bad_continuation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py index f0787de..351a03a 100644 --- a/test/input/func_bad_continuation.py +++ b/test/input/func_bad_continuation.py @@ -3,13 +3,13 @@ __revision__ = 1 # Various alignment for brackets -print [ +LIST0 = [ 1, 2, 3 ] -print [ +LIST1 = [ 1, 2, 3 ] -print [ +LIST2 = [ 1, 2, 3 ] # [bad-continuation] -- cgit v1.2.1 From 751762c8ed3858e3be0a256e1197aaf9d4328664 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Sat, 26 Apr 2014 00:52:19 +0200 Subject: Make the test suite pass on Python 2.6 again. --- checkers/format.py | 2 +- test/input/func_bad_cont_dictcomp_py27.py | 38 +++++++++++++++++++++++++++ test/input/func_bad_continuation.py | 35 ------------------------ test/messages/func_bad_cont_dictcomp_py27.txt | 6 +++++ test/messages/func_bad_continuation.txt | 30 +++++++++------------ test/messages/func_format_py_27.txt | 4 +-- test/unittest_checker_typecheck.py | 6 ++--- 7 files changed, 62 insertions(+), 59 deletions(-) create mode 100644 test/input/func_bad_cont_dictcomp_py27.py create mode 100644 test/messages/func_bad_cont_dictcomp_py27.txt diff --git a/checkers/format.py b/checkers/format.py index c02986e..eedce59 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -229,7 +229,7 @@ _CONTINUATION_MSG_PARTS = { def _Offsets(*args): """Valid indentation offsets for a continued line.""" - return {a: None for a in args} + return dict((a, None) for a in args) def _BeforeBlockOffsets(single, with_body): diff --git a/test/input/func_bad_cont_dictcomp_py27.py b/test/input/func_bad_cont_dictcomp_py27.py new file mode 100644 index 0000000..a552710 --- /dev/null +++ b/test/input/func_bad_cont_dictcomp_py27.py @@ -0,0 +1,38 @@ +"""Bad continuations in dictionary comprehensions.""" + +__revision__ = 0 + +# Dictionary comprehensions should not require extra indentation when breaking +# before the 'for', which is not part of the value +C1 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} + +C2 = {'key{}'.format(x): 'value{}'.format(x) for x in + range(3)} + +# Dictionary comprehensions with multiple loops broken in different places +C3 = {x*y: (x, y) for x in range(3) for y in range(3)} + +C4 = {x*y: (x, y) + for x in range(3) for y in range(3)} + +C5 = {x*y: (x, y) for x + in range(3) for y in range(3)} + +C6 = {x*y: (x, y) for x in range(3) + for y in range(3)} + +C7 = {key: + key ** 2 + for key in range(10)} + +C8 = { + key: key ** 2 + for key in range(10)} + +# Misaligned cases for dict comprehensions +C9 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} # [bad-continuation] + +C9 = {'key{}'.format(x): 'value{}'.format(x) + for x in range(3)} # [bad-continuation] diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py index 351a03a..03beaf4 100644 --- a/test/input/func_bad_continuation.py +++ b/test/input/func_bad_continuation.py @@ -89,41 +89,6 @@ W9 = {'key1': 'value1', # [bad-continuation] } -# Dictionary comprehensions should not require extra indentation when breaking -# before the 'for', which is not part of the value -C1 = {'key{}'.format(x): 'value{}'.format(x) - for x in range(3)} - -C2 = {'key{}'.format(x): 'value{}'.format(x) for x in - range(3)} - -# Dictionary comprehensions with multiple loops broken in different places -C3 = {x*y: (x, y) for x in range(3) for y in range(3)} - -C4 = {x*y: (x, y) - for x in range(3) for y in range(3)} - -C5 = {x*y: (x, y) for x - in range(3) for y in range(3)} - -C6 = {x*y: (x, y) for x in range(3) - for y in range(3)} - -C7 = {key: - key ** 2 - for key in range(10)} - -C8 = { - key: key ** 2 - for key in range(10)} - -# Misaligned cases for dict comprehensions -C9 = {'key{}'.format(x): 'value{}'.format(x) - for x in range(3)} # [bad-continuation] - -C9 = {'key{}'.format(x): 'value{}'.format(x) - for x in range(3)} # [bad-continuation] - # Alignment of arguments in function definitions def continue1(some_arg, some_other_arg): diff --git a/test/messages/func_bad_cont_dictcomp_py27.txt b/test/messages/func_bad_cont_dictcomp_py27.txt new file mode 100644 index 0000000..7443d55 --- /dev/null +++ b/test/messages/func_bad_cont_dictcomp_py27.txt @@ -0,0 +1,6 @@ +C: 35: Wrong continued indentation. + for x in range(3)} # [bad-continuation] + ^ | +C: 38: Wrong continued indentation. + for x in range(3)} # [bad-continuation] + | ^ diff --git a/test/messages/func_bad_continuation.txt b/test/messages/func_bad_continuation.txt index ba6516d..42087e5 100644 --- a/test/messages/func_bad_continuation.txt +++ b/test/messages/func_bad_continuation.txt @@ -28,43 +28,37 @@ C: 85: Wrong hanging indentation in dict value. C: 89: Wrong hanging indentation in dict value. 'value1', # [bad-continuation] ^ | | -C:122: Wrong continued indentation. - for x in range(3)} # [bad-continuation] - ^ | -C:125: Wrong continued indentation. - for x in range(3)} # [bad-continuation] - | ^ -C:141: Wrong hanging indentation before block. +C:106: Wrong hanging indentation before block. some_arg, # [bad-continuation] ^ | -C:142: Wrong hanging indentation before block. +C:107: Wrong hanging indentation before block. some_other_arg): # [bad-continuation] ^ | -C:146:continue4: Missing function docstring -C:162: Wrong continued indentation. +C:111:continue4: Missing function docstring +C:127: Wrong continued indentation. "b") # [bad-continuation] ^ | -C:176: Wrong hanging indentation before block. +C:141: Wrong hanging indentation before block. ): pass # [bad-continuation] | ^| -C:179: Wrong continued indentation before block. +C:144: Wrong continued indentation before block. 2): # [bad-continuation] ^ | -C:187: Wrong continued indentation. +C:152: Wrong continued indentation. 2 and # [bad-continuation] | ^ -C:192: Wrong hanging indentation before block. +C:157: Wrong hanging indentation before block. 2): pass # [bad-continuation] ^ | | -C:199: Wrong continued indentation before block. +C:164: Wrong continued indentation before block. 2 or # [bad-continuation] |^ | -C:203: Wrong continued indentation before block. +C:168: Wrong continued indentation before block. 2): pass # [bad-continuation] ^ | | -C:209: Wrong hanging indentation before block. +C:174: Wrong hanging indentation before block. 2): # [bad-continuation] ^ | | -C:220: Wrong continued indentation. +C:185: Wrong continued indentation. 2): # [bad-continuation] ^ | diff --git a/test/messages/func_format_py_27.txt b/test/messages/func_format_py_27.txt index 20cba32..ae114eb 100644 --- a/test/messages/func_format_py_27.txt +++ b/test/messages/func_format_py_27.txt @@ -41,5 +41,5 @@ C: 71: Exactly one space required before assignment C: 73: Exactly one space required around assignment ocount[obj.__class__]=1 ^ -C:100: More than one statement on a single line -C:103: More than one statement on a single line +C: 99: More than one statement on a single line +C:102: More than one statement on a single line diff --git a/test/unittest_checker_typecheck.py b/test/unittest_checker_typecheck.py index 352b6f9..0a8687b 100644 --- a/test/unittest_checker_typecheck.py +++ b/test/unittest_checker_typecheck.py @@ -13,14 +13,14 @@ class TypeCheckerTest(CheckerTestCase): """ node = test_utils.extract_node(""" - import argparse - argparse.THIS_does_not_EXIST + import optparse + optparse.THIS_does_not_EXIST """) with self.assertAddsMessages( Message( 'no-member', node=node, - args=('Module', 'argparse', 'THIS_does_not_EXIST'))): + args=('Module', 'optparse', 'THIS_does_not_EXIST'))): self.checker.visit_getattr(node) @set_config(ignored_modules=('argparse',)) -- cgit v1.2.1 From 14c199bb7ee187e67b3dac216e71695c5090604e Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 25 Apr 2014 23:31:38 +0200 Subject: Use Module.future_imports for detecting if print is a builtin. --- checkers/format.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/checkers/format.py b/checkers/format.py index eedce59..8b73049 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -474,10 +474,8 @@ class FormatChecker(BaseTokenChecker): def process_module(self, module): self._keywords_with_parens = set() - for node in module.body: - if (isinstance(node, nodes.From) and node.modname == '__future__' - and any(name == 'print_function' for name, _ in node.names)): - self._keywords_with_parens.add('print') + if 'print_function' in module.future_imports: + self._keywords_with_parens.add('print') def _check_keyword_parentheses(self, tokens, start): """Check that there are not unnecessary parens after a keyword. -- cgit v1.2.1 From 2e152035e8e2c56e2461d3f694ebee3713698767 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 18 Apr 2014 18:44:53 +0200 Subject: Do not warn about \u escapes in string literals when Unicode literals are used for Python 2.*. Fixes BitBucket issue #151. --- ChangeLog | 3 +++ checkers/strings.py | 9 ++++++--- test/input/func_unicode_literal_py_30.py | 7 +++++++ test/messages/func_unicode_literal_py_30.txt | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 test/input/func_unicode_literal_py_30.py create mode 100644 test/messages/func_unicode_literal_py_30.txt diff --git a/ChangeLog b/ChangeLog index d57133d..371b0ba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,6 +11,9 @@ ChangeLog for Pylint * Added a new warning for closing over variables that are defined in loops. Fixes Bitbucket issue #176. + * Do not warn about \u escapes in string literals when Unicode literals + are used for Python 2.*. Fixes BitBucket issue #151. + * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. diff --git a/checkers/strings.py b/checkers/strings.py index 663d61d..04cf1bc 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -23,7 +23,7 @@ import tokenize import astroid -from pylint.interfaces import ITokenChecker, IAstroidChecker +from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker from pylint.checkers import BaseChecker, BaseTokenChecker from pylint.checkers import utils from pylint.checkers.utils import check_messages @@ -195,7 +195,7 @@ class StringMethodsChecker(BaseChecker): class StringConstantChecker(BaseTokenChecker): """Check string literals""" - __implements__ = (ITokenChecker,) + __implements__ = (ITokenChecker, IRawChecker) name = 'string_constant' msgs = { 'W1401': ('Anomalous backslash in string: \'%s\'. ' @@ -221,6 +221,9 @@ class StringConstantChecker(BaseTokenChecker): # Unicode strings. UNICODE_ESCAPE_CHARACTERS = 'uUN' + def process_module(self, module): + self._unicode_literals = 'unicode_literals' in module.future_imports + def process_tokens(self, tokens): for (tok_type, token, (start_row, start_col), _, _) in tokens: if tok_type == tokenize.STRING: @@ -279,7 +282,7 @@ class StringConstantChecker(BaseTokenChecker): if next_char in self.UNICODE_ESCAPE_CHARACTERS: if 'u' in prefix: pass - elif _PY3K and 'b' not in prefix: + elif (_PY3K or self._unicode_literals) and 'b' not in prefix: pass # unicode by default else: self.add_message('anomalous-unicode-escape-in-string', diff --git a/test/input/func_unicode_literal_py_30.py b/test/input/func_unicode_literal_py_30.py new file mode 100644 index 0000000..fa47902 --- /dev/null +++ b/test/input/func_unicode_literal_py_30.py @@ -0,0 +1,7 @@ +"""Unicode literals in Python 2.*""" +from __future__ import unicode_literals + +__revision__ = 0 + +BAD_STRING = b'\u1234' +GOOD_STRING = '\u1234' diff --git a/test/messages/func_unicode_literal_py_30.txt b/test/messages/func_unicode_literal_py_30.txt new file mode 100644 index 0000000..25efa99 --- /dev/null +++ b/test/messages/func_unicode_literal_py_30.txt @@ -0,0 +1 @@ +W: 6: Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. -- cgit v1.2.1 From 909f52765007fbd35c024fe0f9dbef99ab83c3e8 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Mon, 28 Apr 2014 22:04:35 +0200 Subject: Make the func_unicode_literal test pass on Python < 2.7.4, which does not tokenize strings with the b prefix properly. --- test/input/func_unicode_literal.py | 7 +++++++ test/input/func_unicode_literal_py_30.py | 7 ------- test/messages/func_unicode_literal_py26.txt | 0 test/messages/func_unicode_literal_py274.txt | 1 + test/messages/func_unicode_literal_py_30.txt | 1 - testutils.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 test/input/func_unicode_literal.py delete mode 100644 test/input/func_unicode_literal_py_30.py create mode 100644 test/messages/func_unicode_literal_py26.txt create mode 100644 test/messages/func_unicode_literal_py274.txt delete mode 100644 test/messages/func_unicode_literal_py_30.txt diff --git a/test/input/func_unicode_literal.py b/test/input/func_unicode_literal.py new file mode 100644 index 0000000..fa47902 --- /dev/null +++ b/test/input/func_unicode_literal.py @@ -0,0 +1,7 @@ +"""Unicode literals in Python 2.*""" +from __future__ import unicode_literals + +__revision__ = 0 + +BAD_STRING = b'\u1234' +GOOD_STRING = '\u1234' diff --git a/test/input/func_unicode_literal_py_30.py b/test/input/func_unicode_literal_py_30.py deleted file mode 100644 index fa47902..0000000 --- a/test/input/func_unicode_literal_py_30.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Unicode literals in Python 2.*""" -from __future__ import unicode_literals - -__revision__ = 0 - -BAD_STRING = b'\u1234' -GOOD_STRING = '\u1234' diff --git a/test/messages/func_unicode_literal_py26.txt b/test/messages/func_unicode_literal_py26.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/messages/func_unicode_literal_py274.txt b/test/messages/func_unicode_literal_py274.txt new file mode 100644 index 0000000..25efa99 --- /dev/null +++ b/test/messages/func_unicode_literal_py274.txt @@ -0,0 +1 @@ +W: 6: Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. diff --git a/test/messages/func_unicode_literal_py_30.txt b/test/messages/func_unicode_literal_py_30.txt deleted file mode 100644 index 25efa99..0000000 --- a/test/messages/func_unicode_literal_py_30.txt +++ /dev/null @@ -1 +0,0 @@ -W: 6: Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. diff --git a/testutils.py b/testutils.py index 8c01ee6..fb8170a 100644 --- a/testutils.py +++ b/testutils.py @@ -38,7 +38,7 @@ from pylint.lint import PyLinter # Utils -SYS_VERS_STR = '%d%d' % sys.version_info[:2] +SYS_VERS_STR = '%d%d%d' % sys.version_info[:3] TITLE_UNDERLINES = ['', '=', '-', '.'] PREFIX = abspath(dirname(__file__)) PY3K = sys.version_info[0] == 3 -- cgit v1.2.1 From 204db940a477a6c849bc325c525c1c4e68d42ccf Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Tue, 29 Apr 2014 11:05:28 -0500 Subject: Fix running init-hook provided by configuration file. --- lint.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lint.py b/lint.py index 679ee69..57accd4 100644 --- a/lint.py +++ b/lint.py @@ -39,7 +39,7 @@ from logilab.common.configuration import UnsupportedAction, OptionsManagerMixIn from logilab.common.optik_ext import check_csv from logilab.common.modutils import load_module_from_name, get_module_part from logilab.common.interface import implements -from logilab.common.textutils import splitstrip +from logilab.common.textutils import splitstrip, unquote from logilab.common.ureports import Table, Text, Section from logilab.common.__pkginfo__ import version as common_version @@ -1009,8 +1009,12 @@ are done by default'''}), linter.disable('suppressed-message') linter.disable('useless-suppression') linter.read_config_file() - # is there some additional plugins in the file configuration, in config_parser = linter.cfgfile_parser + # run init hook, if present, before loading plugins + if config_parser.has_option('MASTER', 'init-hook'): + cb_init_hook('init-hook', + unquote(config_parser.get('MASTER', 'init-hook'))) + # is there some additional plugins in the file configuration, in if config_parser.has_option('MASTER', 'load-plugins'): plugins = splitstrip(config_parser.get('MASTER', 'load-plugins')) linter.load_plugin_modules(plugins) -- cgit v1.2.1 From d69c458b87c52c8af173979e6583ca6b0aa5ed3c Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Wed, 30 Apr 2014 08:23:58 -0500 Subject: Update ChangeLog for init-hook fix. --- ChangeLog | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog b/ChangeLog index 371b0ba..a073050 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,9 @@ ChangeLog for Pylint ==================== -- + * Restore the ability to specify the init-hook option via the + configuration file, which was accidentally broken in 1.2.0. + * Add a new warning [bad-continuation] for badly indentend continued lines. -- cgit v1.2.1 From e772fe51ac49b3e00b119f6ab9c819a9f50c3278 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Tue, 29 Apr 2014 13:08:19 +0200 Subject: fix explicit check of python script. Closes #219 --- ChangeLog | 3 +++ lint.py | 2 +- test/unittest_lint.py | 13 +++++++++++-- utils.py | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index a073050..9bec428 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,12 +20,15 @@ ChangeLog for Pylint * Extend the checking for unbalanced-tuple-unpacking and unpacking-non-sequence to instance attribute unpacking as well. + * Fix explicit checking of python script (1.2 regression, #219) + * Restore --init-hook, renamed accidentally into --init-hooks in 1.2.0 (#211) * Add 'indexing-exception' warning, which detects that indexing an exception occurs in Python 2 (behaviour removed in Python 3). + 2014-04-18 -- 1.2.0 * Pass the current python paths to pylint process when invoked via epylint. Fixes BitBucket issue #133. diff --git a/lint.py b/lint.py index 57accd4..529fbd4 100644 --- a/lint.py +++ b/lint.py @@ -607,7 +607,7 @@ warning, statement which respectively contain the number of errors / warnings\ # build ast and check modules or packages for descr in self.expand_files(files_or_modules): modname, filepath = descr['name'], descr['path'] - if not self.should_analyze_file(modname, filepath): + if not descr['isarg'] and not self.should_analyze_file(modname, filepath): continue if self.config.files_output: reportfile = 'pylint_%s.%s' % (modname, self.reporter.extension) diff --git a/test/unittest_lint.py b/test/unittest_lint.py index 2b13039..7dd403b 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -279,8 +279,9 @@ class PyLinterTC(TestCase): self.linter.set_reporter(text.TextReporter()) self.linter.config.files_output = True self.linter.should_analyze_file = lambda *args: False - self.linter.check('os') - self.assertFalse(os.path.exists('pylint_os.txt')) + self.linter.check('logilab') + self.assertTrue(os.path.exists('pylint_logilab.txt')) + self.assertFalse(os.path.exists('pylint_logilab_common.txt')) def test_enable_report(self): self.assertEqual(self.linter.report_is_enabled('RP0001'), True) @@ -385,6 +386,14 @@ class PyLinterTC(TestCase): self.assertRaises(RuntimeError, Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant']) + + def test_analyze_explicit_script(self): + self.linter.set_reporter(TestReporter()) + self.linter.check(self.datapath('ascript')) + self.assertEqual( + ['C: 2: Line too long (175/80)'], + self.linter.reporter.messages) + class ConfigTC(TestCase): def setUp(self): diff --git a/utils.py b/utils.py index aee119e..c6a7371 100644 --- a/utils.py +++ b/utils.py @@ -638,7 +638,7 @@ def expand_modules(files_or_modules, black_list): errors.append({'key': 'fatal', 'mod': modname, 'ex': ex}) continue filepath = normpath(filepath) - result.append({'path': filepath, 'name': modname, + result.append({'path': filepath, 'name': modname, 'isarg': True, 'basepath': filepath, 'basename': modname}) if not (modname.endswith('.__init__') or modname == '__init__') \ and '__init__.py' in filepath: @@ -647,6 +647,7 @@ def expand_modules(files_or_modules, black_list): continue submodname = '.'.join(modpath_from_file(subfilepath)) result.append({'path': subfilepath, 'name': submodname, + 'isarg': False, 'basepath': filepath, 'basename': modname}) return result, errors -- cgit v1.2.1 From a9f6d54370ecd2ca58106c974038fb2a81ff0b38 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 30 Apr 2014 17:56:38 +0200 Subject: 1.2.1 --- ChangeLog | 3 ++- __pkginfo__.py | 2 +- debian/changelog | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 9bec428..da62285 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,7 @@ ChangeLog for Pylint ==================== --- +2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. @@ -29,6 +29,7 @@ ChangeLog for Pylint an exception occurs in Python 2 (behaviour removed in Python 3). + 2014-04-18 -- 1.2.0 * Pass the current python paths to pylint process when invoked via epylint. Fixes BitBucket issue #133. diff --git a/__pkginfo__.py b/__pkginfo__.py index d0a9aac..86488fa 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -19,7 +19,7 @@ import sys modname = distname = 'pylint' -numversion = (1, 2, 0) +numversion = (1, 2, 1) version = '.'.join([str(num) for num in numversion]) if sys.version_info < (2, 6): diff --git a/debian/changelog b/debian/changelog index fd43e38..a86f17b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +pylint (1.2.1-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Wed, 30 Apr 2014 17:56:29 +0200 + pylint (1.2.0-1) unstable; urgency=low * new upstream release -- cgit v1.2.1 From 650f0ee844d5d6b6cf49caa2888304b3707b66b2 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 30 Apr 2014 18:03:04 +0200 Subject: missing test file --- test/data/ascript | 2 ++ 1 file changed, 2 insertions(+) create mode 100755 test/data/ascript diff --git a/test/data/ascript b/test/data/ascript new file mode 100755 index 0000000..f401ebc --- /dev/null +++ b/test/data/ascript @@ -0,0 +1,2 @@ +#!/usr/bin/python +"""ttttttttttttttttttttoooooooooooooooooooooooooooooooooooooooooooooooooooooo lllllllllllooooooooooooooooooooooooooooonnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnngggggggggggggggggg""" -- cgit v1.2.1 From 6d031115a314d9f8cf663b6021adde79ede8a2f5 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Wed, 30 Apr 2014 20:07:39 +0200 Subject: Make sure the script used in testing is installed, otherwise the tox tests fail. --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 37c6e67..8c28721 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,3 +10,5 @@ recursive-include doc *.rst *.jpeg Makefile *.html *.py recursive-include test *.py *.txt *.dot *.sh include test/input/similar* include test/input/noext +include test/data/ascript + -- cgit v1.2.1 From f44c9a1cdcf3c9983a712672e0342cb8dd489f7a Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 2 May 2014 09:36:01 +0300 Subject: Fix unused-import false positive with Python 3 metaclasses. Closes issue #143. --- ChangeLog | 3 +++ checkers/variables.py | 16 ++++++++++++++++ test/input/func_unused_import_py30.py | 20 ++++++++++++++++++++ test/messages/func_unused_import_py30.txt | 6 ++++++ 4 files changed, 45 insertions(+) create mode 100644 test/input/func_unused_import_py30.py create mode 100644 test/messages/func_unused_import_py30.txt diff --git a/ChangeLog b/ChangeLog index da62285..9b675d0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,9 @@ ChangeLog for Pylint ==================== +-- + * Fix unused-import false positive with Python 3 metaclasses (#143). + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/variables.py b/checkers/variables.py index a6123de..dc8d111 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -716,6 +716,22 @@ class VariablesChecker3k(VariablesChecker): # do not check for not used locals here self._to_consume.pop() + def leave_module(self, node): + """ Update consumption analysis variable + for metaclasses. + """ + for klass in node.nodes_of_class(astroid.Class): + if klass._metaclass: + metaclass = klass.metaclass() + module_locals = self._to_consume[0][0] + + if isinstance(klass._metaclass, astroid.Name): + module_locals.pop(klass._metaclass.name, None) + if metaclass: + # if it uses a `metaclass=module.Class` + module_locals.pop(metaclass.root().name, None) + super(VariablesChecker3k, self).leave_module(node) + if sys.version_info >= (3, 0): VariablesChecker = VariablesChecker3k diff --git a/test/input/func_unused_import_py30.py b/test/input/func_unused_import_py30.py new file mode 100644 index 0000000..ae6db93 --- /dev/null +++ b/test/input/func_unused_import_py30.py @@ -0,0 +1,20 @@ +"""check unused import for metaclasses +""" +__revision__ = 1 +import abc +import sys +from abc import ABCMeta +from abc import ABCMeta as SomethingElse + +class Meta(metaclass=abc.ABCMeta): + """ Test """ + def __init__(self): + self.data = sys.executable + self.test = abc + +class Meta2(metaclass=ABCMeta): + """ Test """ + +class Meta3(metaclass=SomethingElse): + """ test """ + diff --git a/test/messages/func_unused_import_py30.txt b/test/messages/func_unused_import_py30.txt new file mode 100644 index 0000000..72a21fe --- /dev/null +++ b/test/messages/func_unused_import_py30.txt @@ -0,0 +1,6 @@ +R: 9:Meta: Too few public methods (0/2) +R: 15:Meta2: Too few public methods (0/2) +R: 18:Meta3: Too few public methods (0/2) +W: 7: Reimport 'ABCMeta' (imported line 6) +W: 15:Meta2: Class has no __init__ method +W: 18:Meta3: Class has no __init__ method \ No newline at end of file -- cgit v1.2.1 From e65c187c17adb9d4dfcd214d483a78c491390f8d Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 2 May 2014 23:38:49 +0300 Subject: Emit 'undefined-variable' for undefined names used as metaclasses with Python 3 `metaclass=` syntax. --- ChangeLog | 3 ++ checkers/variables.py | 52 ++++++++++++++++++---- test/input/func_undefined_metaclass_var_py30.py | 24 ++++++++++ .../messages/func_undefined_metaclass_var_py30.txt | 2 + 4 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 test/input/func_undefined_metaclass_var_py30.py create mode 100644 test/messages/func_undefined_metaclass_var_py30.txt diff --git a/ChangeLog b/ChangeLog index 9b675d0..92faa54 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,9 @@ ChangeLog for Pylint ==================== -- + * Emit 'undefined-variable' for undefined names when using the + Python 3 `metaclass=` argument. + * Fix unused-import false positive with Python 3 metaclasses (#143). 2014-04-30 -- 1.2.1 diff --git a/checkers/variables.py b/checkers/variables.py index dc8d111..75c01cd 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -716,20 +716,54 @@ class VariablesChecker3k(VariablesChecker): # do not check for not used locals here self._to_consume.pop() + @check_messages('undefined-variable') def leave_module(self, node): """ Update consumption analysis variable for metaclasses. """ + module_locals = self._to_consume[0][0] + module_imports = self._to_consume[0][1] + consumed = {} + for klass in node.nodes_of_class(astroid.Class): - if klass._metaclass: - metaclass = klass.metaclass() - module_locals = self._to_consume[0][0] - - if isinstance(klass._metaclass, astroid.Name): - module_locals.pop(klass._metaclass.name, None) - if metaclass: - # if it uses a `metaclass=module.Class` - module_locals.pop(metaclass.root().name, None) + found = metaclass = name = None + metaclass = klass.metaclass() + + # Look the name in the already found locals. + # If it's not found there, look in the module locals + # and in the imported modules. + if isinstance(klass._metaclass, astroid.Name): + name = klass._metaclass.name + elif metaclass: + # if it uses a `metaclass=module.Class` + name = metaclass.root().name + + if name: + found = consumed.setdefault(name, + module_locals.get(name, module_imports.get(name)) + ) + + if found is None: + name = None + if metaclass: + name = metaclass.name + elif isinstance(klass._metaclass, astroid.Name): + name = klass._metaclass.name + elif isinstance(klass._metaclass, astroid.Getattr): + name = klass._metaclass.as_string() + + if name is not None: + if not (name in astroid.Module.scope_attrs or + is_builtin(name) or + name in self.config.additional_builtins or + name in node.locals): + self.add_message('undefined-variable', + node=klass, + args=(name, )) + # Pop the consumed items, in order to + # avoid having unused-import false positives + for name in consumed: + module_locals.pop(name, None) super(VariablesChecker3k, self).leave_module(node) if sys.version_info >= (3, 0): diff --git a/test/input/func_undefined_metaclass_var_py30.py b/test/input/func_undefined_metaclass_var_py30.py new file mode 100644 index 0000000..276b213 --- /dev/null +++ b/test/input/func_undefined_metaclass_var_py30.py @@ -0,0 +1,24 @@ +"""test access to undefined variables in Python 3 metaclass syntax """ +# pylint: disable=no-init, invalid-name, too-few-public-methods +__revision__ = '$Id:' + +import abc +from abc import ABCMeta + +class Bad(metaclass=ABCMet): + """ Notice the typo """ + +class SecondBad(metaclass=ab.ABCMeta): + """ Notice the `ab` module. """ + +class Good(metaclass=int): + """ int is not a proper metaclass, but it is defined. """ + +class SecondGood(metaclass=Good): + """ empty """ + +class ThirdGood(metaclass=ABCMeta): + """ empty """ + +data = abc +testdata = ABCMeta diff --git a/test/messages/func_undefined_metaclass_var_py30.txt b/test/messages/func_undefined_metaclass_var_py30.txt new file mode 100644 index 0000000..a82ac6a --- /dev/null +++ b/test/messages/func_undefined_metaclass_var_py30.txt @@ -0,0 +1,2 @@ +E: 8:Bad: Undefined variable 'ABCMet' +E: 11:SecondBad: Undefined variable 'ab.ABCMeta' \ No newline at end of file -- cgit v1.2.1 From 0babd7e4bdc86b1c06791d6bc933267eda0d6f26 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 3 May 2014 00:21:15 +0300 Subject: Fix a false positive with classes without explictly defined metaclasses (inherited from ancestors). --- checkers/variables.py | 5 +++ test/input/func_undefined_metaclass_var_py30.py | 47 +++++++++++++------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/checkers/variables.py b/checkers/variables.py index 75c01cd..f923721 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -727,6 +727,11 @@ class VariablesChecker3k(VariablesChecker): for klass in node.nodes_of_class(astroid.Class): found = metaclass = name = None + if not klass._metaclass: + # Skip if this class doesn't use + # explictly a metaclass, but inherits it from ancestors + continue + metaclass = klass.metaclass() # Look the name in the already found locals. diff --git a/test/input/func_undefined_metaclass_var_py30.py b/test/input/func_undefined_metaclass_var_py30.py index 276b213..307a431 100644 --- a/test/input/func_undefined_metaclass_var_py30.py +++ b/test/input/func_undefined_metaclass_var_py30.py @@ -1,24 +1,27 @@ """test access to undefined variables in Python 3 metaclass syntax """ # pylint: disable=no-init, invalid-name, too-few-public-methods -__revision__ = '$Id:' - -import abc -from abc import ABCMeta - -class Bad(metaclass=ABCMet): - """ Notice the typo """ - -class SecondBad(metaclass=ab.ABCMeta): - """ Notice the `ab` module. """ - -class Good(metaclass=int): - """ int is not a proper metaclass, but it is defined. """ - -class SecondGood(metaclass=Good): - """ empty """ - -class ThirdGood(metaclass=ABCMeta): - """ empty """ - -data = abc -testdata = ABCMeta +__revision__ = '$Id:' + +import abc +from abc import ABCMeta + +class Bad(metaclass=ABCMet): + """ Notice the typo """ + +class SecondBad(metaclass=ab.ABCMeta): + """ Notice the `ab` module. """ + +class Good(metaclass=int): + """ int is not a proper metaclass, but it is defined. """ + +class SecondGood(metaclass=Good): + """ empty """ + +class ThirdGood(metaclass=ABCMeta): + """ empty """ + +class FourthGood(ThirdGood): + """ This should not trigger anything. """ + +data = abc +testdata = ABCMeta -- cgit v1.2.1 From 7f5869347f70a4af11587a8975e262e9c813386b Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 5 May 2014 13:03:43 +0300 Subject: If the metaclass was infered properly, skip the undefined-variable check. --- checkers/variables.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/checkers/variables.py b/checkers/variables.py index f923721..ebe520a 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -748,11 +748,9 @@ class VariablesChecker3k(VariablesChecker): module_locals.get(name, module_imports.get(name)) ) - if found is None: + if found is None and not metaclass: name = None - if metaclass: - name = metaclass.name - elif isinstance(klass._metaclass, astroid.Name): + if isinstance(klass._metaclass, astroid.Name): name = klass._metaclass.name elif isinstance(klass._metaclass, astroid.Getattr): name = klass._metaclass.as_string() -- cgit v1.2.1 From b323833f58b9216c4d2432f33c61f0c852483df1 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 5 May 2014 16:55:27 +0300 Subject: Fix a potential AttributeError when checking for constructor arguments. --- checkers/typecheck.py | 2 +- test/input/func_arguments.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 4cb406f..79774de 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -98,7 +98,7 @@ def _determine_callable(callable_obj): except astroid.NotFoundError: new = None - if not new or new.parent.name == 'object': + if not new or new.parent.scope().name == 'object': try: # Use the last definition of __init__. callable_obj = callable_obj.local_attr('__init__')[-1] diff --git a/test/input/func_arguments.py b/test/input/func_arguments.py index 1c27ef2..b607699 100644 --- a/test/input/func_arguments.py +++ b/test/input/func_arguments.py @@ -81,3 +81,20 @@ def method_tests(): demo.decorated_method() DemoClass.decorated_method(demo) +# Test a regression (issue #234) +import sys + +# pylint: disable=too-few-public-methods +class Text(object): + """ Regression """ + + if sys.version_info > (3,): + def __new__(cls): + """ empty """ + return object.__new__(cls) + else: + def __new__(cls): + """ empty """ + return object.__new__(cls) + +Text() -- cgit v1.2.1 From 648d5d94d70cebe1cd4ae339ccabefbd126961bc Mon Sep 17 00:00:00 2001 From: Steven Myint Date: Thu, 8 May 2014 06:32:29 -0700 Subject: Fix spelling error --- checkers/typecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 79774de..25f7612 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -139,7 +139,7 @@ class should be ignored. A mixin class is detected if its name ends with \ 'metavar': '', 'help': 'List of module names for which member attributes \ should not be checked (useful for modules/projects where namespaces are \ -manipulated during runtime and thus extisting member attributes cannot be \ +manipulated during runtime and thus existing member attributes cannot be \ deduced by static analysis'}, ), ('ignored-classes', -- cgit v1.2.1 From 18c6577b4dacdf5eb1d4ecb5a67c3e4faa489212 Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 20 May 2014 16:47:18 +0300 Subject: Document the fact that Pylint does not import modules. Closes issue #249. --- doc/run.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/run.rst b/doc/run.rst index 4e752b0..d4a2aa9 100644 --- a/doc/run.rst +++ b/doc/run.rst @@ -10,9 +10,11 @@ Pylint is meant to be called from the command line. The usage is :: pylint [options] module_or_package You should give Pylint the name of a python package or module. Pylint -will ``import`` this package or module, so you should pay attention to -your ``PYTHONPATH``, since it is a common error to analyze an -installed version of a module instead of the development version. +``will not import`` this package or module, though uses Python internals +to locate them and as such is subject to the same rules and configuration. +You should pay attention to your ``PYTHONPATH``, since it is a common error +to analyze an installed version of a module instead of the +development version. It is also possible to analyze python files, with a few restrictions. The thing to keep in mind is that Pylint will try to -- cgit v1.2.1 From 6c105d2c5db828ae94218255a735820324c950ab Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 27 May 2014 23:19:03 +0300 Subject: Don't crash when sorting visible objects other than function nodes. Closes issue #252. --- pyreverse/diagrams.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyreverse/diagrams.py b/pyreverse/diagrams.py index 6dde9a1..28cc500 100644 --- a/pyreverse/diagrams.py +++ b/pyreverse/diagrams.py @@ -89,8 +89,11 @@ class ClassDiagram(Figure, FilterMixIn): def get_methods(self, node): """return visible methods""" - return [m for m in sorted(node.values(), key=lambda n: n.name) - if isinstance(m, astroid.Function) and self.show_attr(m.name)] + methods = [ + m for m in node.values() + if isinstance(m, astroid.Function) and self.show_attr(m.name) + ] + return sorted(methods, key=lambda n: n.name) def add_object(self, title, node): """create a diagram object -- cgit v1.2.1 From 3d2615833bb7a37d65e7ac780e3c4bcdf8c7049d Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 7 Jun 2014 17:41:02 +0300 Subject: Drop Python 2.6 support, other minor fixes. --- checkers/strings.py | 61 +++++++++++++++---------------- test/input/func_string_format_py26.py | 16 -------- test/input/func_string_format_py27.py | 18 +++++++++ test/messages/func_string_format_py26.txt | 13 ------- test/messages/func_string_format_py27.txt | 13 +++++++ 5 files changed, 61 insertions(+), 60 deletions(-) delete mode 100644 test/input/func_string_format_py26.py create mode 100644 test/input/func_string_format_py27.py delete mode 100644 test/messages/func_string_format_py26.txt create mode 100644 test/messages/func_string_format_py27.txt diff --git a/checkers/strings.py b/checkers/strings.py index ad9d70c..e02e79f 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -29,9 +29,8 @@ from pylint.checkers import BaseChecker, BaseTokenChecker from pylint.checkers import utils from pylint.checkers.utils import check_messages -_PY3K = sys.version_info >= (3, 0) -_PY26 = sys.version_info == (2, 7) -_GT_PY25 = sys.version_info > (2, 5) +_PY3K = sys.version_info[:2] >= (3, 0) +_PY27 = sys.version_info[:2] == (2, 7) MSGS = { 'E1300': ("Unsupported format character %r (%#02x) at index %d", @@ -280,57 +279,57 @@ class StringMethodsChecker(BaseChecker): self.add_message('bad-str-strip-call', node=node, args=(func.bound.name, func.name)) elif func.name == 'format': - if _GT_PY25: - self._parse_format(node, func) + if _PY27 or _PY3K: + self._check_new_format(node, func) - def _parse_format(self, node, func): + def _check_new_format(self, node, func): + """ Check the new string formatting. """ try: strnode = func.bound.infer().next() except astroid.InferenceError: return - try: positional, named = get_args(node) except astroid.InferenceError: return - try: - required_keys, required_num_args = \ - parse_format_method_string(strnode.value) + specifiers, num_args = parse_format_method_string(strnode.value) except utils.IncompleteFormatString: self.add_message('bad-format-string', node=node) return - manual_keys = set([key for key in required_keys - if key.isdigit()]) - if manual_keys and required_num_args: + manual_keys = {key[0] for key in specifiers + if isinstance(key[0], int)} + named_keys = {key[0] for key in specifiers + if isinstance(key[0], str)} + if manual_keys and num_args: self.add_message('format-combined-specifiers', node=node) return - if _PY26 and required_num_args: - self.add_message('no-automatic-field-numbering', - node=node) - - if required_keys: - for key in required_keys: - if key not in named and not key.isdigit(): - self.add_message('missing-format-argument-key', + if named_keys: + for key in named_keys: + if key not in named: + self.add_message('missing-format-argument-key', node=node, args=(key, )) for key in named: - if key not in required_keys: + if key not in named_keys: self.add_message('unused-format-string-argument', node=node, - args=(key, )) + args=(key, )) else: - if positional > required_num_args: - self.add_message('too-many-format-args', node=node) - elif positional < required_num_args: - self.add_message('too-few-format-args', node=node) - + if positional > num_args: + # We can have two possibilities: + # * "{0} {1}".format(a, b) + # * "{} {} {}".format(a, b, c, d) + # We can check the manual keys for the first one. + if len(manual_keys) != positional: + self.add_message('too-many-format-args', node=node) + elif positional < num_args: + self.add_message('too-few-format-args', node=node) if manual_keys and positional < len(manual_keys): - self.add_message('too-few-format-args', node=node) + self.add_message('too-few-format-args', node=node) class StringConstantChecker(BaseTokenChecker): """Check string literals""" @@ -424,10 +423,10 @@ class StringConstantChecker(BaseTokenChecker): elif (_PY3K or self._unicode_literals) and 'b' not in prefix: pass # unicode by default else: - self.add_message('anomalous-unicode-escape-in-string', + self.add_message('anomalous-unicode-escape-in-string', line=start_row, args=(match, )) elif next_char not in self.ESCAPE_CHARACTERS: - self.add_message('anomalous-backslash-in-string', + 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 diff --git a/test/input/func_string_format_py26.py b/test/input/func_string_format_py26.py deleted file mode 100644 index 33c5798..0000000 --- a/test/input/func_string_format_py26.py +++ /dev/null @@ -1,16 +0,0 @@ -"""test for Python 3 string formatting error -""" - -__revision__ = 1 - -def pprint(): - """Test string format """ - print "{} {".format() - print "{} }".format() - print "{} {a}".format(1, 2) - print "{} {}".format(1) - print "{} {}".format(1, 2, 3) - print "{a} {b}".format(a=1, c=2) - print "{a} {b} {c}".format() - print "{} {}".format(a=1, b=2) - print "{a} {b}".format(1, 2) diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py new file mode 100644 index 0000000..6922337 --- /dev/null +++ b/test/input/func_string_format_py27.py @@ -0,0 +1,18 @@ +"""test for Python 3 string formatting error +""" + +__revision__ = 1 + +def pprint(): + """Test string format """ + print "{} {".format() + print "{} }".format() + print "{0} {1}".format(1, 2) + print "{0} {1} {a}".format(1, 2, 3) + print "{a} {b}".format(a=1, c=2) + print "{} {a}".format(1, 2) + print "{} {}".format(1) + print "{} {}".format(1, 2, 3) + print "{a} {b} {c}".format() + print "{} {}".format(a=1, b=2) + print "{a} {b}".format(1, 2) diff --git a/test/messages/func_string_format_py26.txt b/test/messages/func_string_format_py26.txt deleted file mode 100644 index d60ee64..0000000 --- a/test/messages/func_string_format_py26.txt +++ /dev/null @@ -1,13 +0,0 @@ -E: 8:pprint: Invalid Python 3 format string -E: 9:pprint: Invalid Python 3 format string -E: 10:pprint: Mixing named and unnamed conversion specifiers in format string -E: 11:pprint: Not enough arguments for format string -E: 12:pprint: Too many arguments for format string -E: 13:pprint: Missing keyword argument 'b' for format string -E: 14:pprint: Missing keyword argument 'a' for format string -E: 14:pprint: Missing keyword argument 'b' for format string -E: 14:pprint: Missing keyword argument 'c' for format string -E: 15:pprint: Not enough arguments for format string -E: 16:pprint: Missing keyword argument 'a' for format string -E: 16:pprint: Missing keyword argument 'b' for format string -W: 13:pprint: Unused format argument 'c' \ No newline at end of file diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt new file mode 100644 index 0000000..d60ee64 --- /dev/null +++ b/test/messages/func_string_format_py27.txt @@ -0,0 +1,13 @@ +E: 8:pprint: Invalid Python 3 format string +E: 9:pprint: Invalid Python 3 format string +E: 10:pprint: Mixing named and unnamed conversion specifiers in format string +E: 11:pprint: Not enough arguments for format string +E: 12:pprint: Too many arguments for format string +E: 13:pprint: Missing keyword argument 'b' for format string +E: 14:pprint: Missing keyword argument 'a' for format string +E: 14:pprint: Missing keyword argument 'b' for format string +E: 14:pprint: Missing keyword argument 'c' for format string +E: 15:pprint: Not enough arguments for format string +E: 16:pprint: Missing keyword argument 'a' for format string +E: 16:pprint: Missing keyword argument 'b' for format string +W: 13:pprint: Unused format argument 'c' \ No newline at end of file -- cgit v1.2.1 From 96119dbbbcdab4e46ccfde4ed57cca20ee34a86b Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 8 Jun 2014 10:05:30 +0300 Subject: Add support for checking attribute and key lookups in string formatting. Improve the string formatting tests. --- checkers/strings.py | 166 ++++++++++++++++++++++++------ test/input/func_string_format_py27.py | 43 ++++++++ test/messages/func_string_format_py27.txt | 34 +++--- 3 files changed, 198 insertions(+), 45 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index e02e79f..6859ef5 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -21,6 +21,7 @@ import sys import tokenize import string +from collections import deque import astroid @@ -81,28 +82,29 @@ MSGS = { "Used when a Python 3 format string is invalid"), 'E1308': ("Missing keyword argument %r for format string", "missing-format-argument-key", - "Used when a Python 3 format string that uses named specifiers \ + "Used when a Python 3 format string that uses named fields \ doesn't receive one or more required keywords."), 'W1302': ("Unused format argument %r", "unused-format-string-argument", "Used when a Python 3 format string that uses named \ - conversion specifiers is used with an argument that \ + fields is used with an argument that \ is not required by the format string."), - 'W1303': ("Format string contains both automatic field numbering " - "and manual field specifiers", - "format-combined-specifiers", + "and manual field specification", + "format-combined-specification", "Usen when a format string contains both automatic \ field numbering (e.g. '{}') and manual field \ specification (e.g. '{0}')"), - 'W1304': ("Can't use automatic field numbering for this version", - "no-automatic-field-numbering", - "Usen for Python versions lower than 2.7 when a " - "format string is used with automatic field numbering " - "(e.g. '{}'). Only manual field numbering (e.g. '{0}') " - "or named fields (e.g. '{a}') can work.", - {'maxversion': (2, 6)}), - + 'W1304': ("Missing format attribute %r in format specifier %r", + "missing-format-attribute", + "Used when a format string uses an attribute specifier " + "({0.length}), but the argument passed for formatting " + "doesn't have that attribute."), + 'W1305': ("Using invalid lookup key %r in format specifier %r", + "invalid-format-index", + "Used when a format string uses a lookup specifier " + "({a[1]}), but the argument passed for formatting " + "doesn't contain that key.") } OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, @@ -139,6 +141,10 @@ def parse_format_method_string(format_string): name = result[1] if name: keyname, fielditerator = split_format_field_names(name) + if not isinstance(keyname, str): + # In Python 2 it will return long which will lead + # to different output between 2 and 3 + keyname = int(keyname) keys.append((keyname, list(fielditerator))) else: num_args += 1 @@ -164,6 +170,22 @@ def get_args(callfunc): positional += 1 return positional, named +def get_access_path(key, parts): + """ Given a list of format specifiers, returns + the final access path (e.g. a.b.c[0][1]) + """ + parts = parts[:] + parts.reverse() + path = [] + while parts: + is_attribute, specifier = parts.pop() + if is_attribute: + path.append(".{}".format(specifier)) + else: + path.append("[{!r}]".format(specifier)) + return key + "".join(path) + + class StringFormatChecker(BaseChecker): """Checks string formatting operations to ensure that the format string is valid and the arguments match the format string. @@ -293,44 +315,124 @@ class StringMethodsChecker(BaseChecker): except astroid.InferenceError: return try: - specifiers, num_args = parse_format_method_string(strnode.value) + fields, num_args = parse_format_method_string(strnode.value) except utils.IncompleteFormatString: self.add_message('bad-format-string', node=node) return - manual_keys = {key[0] for key in specifiers - if isinstance(key[0], int)} - named_keys = {key[0] for key in specifiers - if isinstance(key[0], str)} - if manual_keys and num_args: - self.add_message('format-combined-specifiers', + manual_fields = {field[0] for field in fields + if isinstance(field[0], int)} + named_fields = {field[0] for field in fields + if isinstance(field[0], str)} + if manual_fields and num_args: + self.add_message('format-combined-specification', node=node) return - if named_keys: - for key in named_keys: - if key not in named: - self.add_message('missing-format-argument-key', + if named_fields: + for field in named_fields: + if field not in named and field: + self.add_message('missing-format-argument-key', node=node, - args=(key, )) - for key in named: - if key not in named_keys: + args=(field, )) + for field in named: + if field not in named_fields: self.add_message('unused-format-string-argument', node=node, - args=(key, )) + args=(field, )) else: if positional > num_args: # We can have two possibilities: # * "{0} {1}".format(a, b) # * "{} {} {}".format(a, b, c, d) # We can check the manual keys for the first one. - if len(manual_keys) != positional: + if len(manual_fields) != positional: self.add_message('too-many-format-args', node=node) elif positional < num_args: - self.add_message('too-few-format-args', node=node) - if manual_keys and positional < len(manual_keys): - self.add_message('too-few-format-args', node=node) + self.add_message('too-few-format-args', node=node) + + if manual_fields and positional < len(manual_fields): + self.add_message('too-few-format-args', node=node) + + self._check_new_format_specifiers(node, fields, named) + def _check_new_format_specifiers(self, node, fields, named): + """ + Check attribute and index access in the format + string ("{0.a}" and "{0[a]}"). + """ + for key, specifiers in fields: + # Obtain the argument. If it can't be obtained + # or infered, skip this check. + if isinstance(key, int): + try: + argument = utils.get_argument_from_call(node, key) + except utils.NoSuchArgumentError: + break + else: + if key not in named: + break + argument = named[key] + try: + argument = next(argument.infer()) + except astroid.InferenceError: + break + if not specifiers: + # No need to check this key if it doesn't + # use attribute / item access + continue + previous = argument + specifiers = deque(specifiers[:]) + parsed = [] + + while specifiers: + is_attribute, specifier = specifiers.popleft() + parsed.append((is_attribute, specifier)) + if is_attribute: + try: + previous = previous.getattr(specifier)[0] + except astroid.NotFoundError: + if (hasattr(previous, 'has_dynamic_getattr') and + previous.has_dynamic_getattr()): + # Can't process further + break + path = get_access_path(key, parsed) + self.add_message('missing-format-attribute', + args=(specifier, path), + node=node) + break + else: + warn_error = False + if hasattr(previous, 'getitem'): + try: + previous = previous.getitem(specifier) + except (IndexError, TypeError): + warn_error = True + else: + try: + # Lookup __getitem__ in the current node, + # but skip further checks, because we can't + # retrieve the looked object + previous.getattr('__getitem__') + break + except NotFoundError: + warn_error = True + if warn_error: + path = get_access_path(key, parsed) + self.add_message('invalid-format-index', + args=(specifier, path), + node=node) + break + + try: + previous = next(previous.infer()) + except astroid.InferenceError: + # can't check further if we can't infer it + break + if previous is astroid.YES: + break + + class StringConstantChecker(BaseTokenChecker): """Check string literals""" __implements__ = (ITokenChecker, IRawChecker) diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index 6922337..7d76662 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -1,12 +1,35 @@ """test for Python 3 string formatting error """ +# pylint: disable=too-few-public-methods, import-error +from missing import Missing __revision__ = 1 +class Custom(object): + """ Has a __getattr__ """ + def __getattr__(self): + return self + +class Test(object): + """ test format attribute access """ + custom = Custom() + ids = [1, 2, 3, [4, 5, 6]] + +class Getitem(object): + """ test custom getitem for lookup access """ + def __getitem__(self, index): + return 42 + +class ReturnYes(object): + """ can't be properly infered """ + missing = Missing() + def pprint(): """Test string format """ + print "{{}}".format(1) print "{} {".format() print "{} }".format() + print "{0} {}".format(1, 2) print "{0} {1}".format(1, 2) print "{0} {1} {a}".format(1, 2, 3) print "{a} {b}".format(a=1, c=2) @@ -16,3 +39,23 @@ def pprint(): print "{a} {b} {c}".format() print "{} {}".format(a=1, b=2) print "{a} {b}".format(1, 2) + print "{0!r:20}".format("Hello") + print "{!r:20}".format("Hello") + print "{a!r:20}".format(a="Hello") + print "{a.test}".format(a=Custom()) + print "{a.__len__}".format(a=[]) + print "{a.ids.__len__}".format(a=Test()) + print "{a.ids.__len__.length}".format(a=Test()) + print "{a[0]}".format(a=Getitem()) + print "{a[0][0]}".format(a=[Getitem()]) + print "{a[0][0]}".format(a=[[1]]) + print "{[0][0]}".format(((1, ))) + print "{[0][0]}".format({0: {0: 1}}) + print "{[0][0]}".format(["test"]) + print "{a.ids[3][1]}".format(a=Test()) + print "{a.ids[3][400]}".format(a=Test()) + print "{a.ids[3]['string']}".format(a=Test()) + print "{[0][1]}".format(["a"]) + print "{0.missing.length}".format(ReturnYes()) + print "{1.missing.length}".format(ReturnYes()) + print "{b[0]}".format(a=23) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index d60ee64..22f072d 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -1,13 +1,21 @@ -E: 8:pprint: Invalid Python 3 format string -E: 9:pprint: Invalid Python 3 format string -E: 10:pprint: Mixing named and unnamed conversion specifiers in format string -E: 11:pprint: Not enough arguments for format string -E: 12:pprint: Too many arguments for format string -E: 13:pprint: Missing keyword argument 'b' for format string -E: 14:pprint: Missing keyword argument 'a' for format string -E: 14:pprint: Missing keyword argument 'b' for format string -E: 14:pprint: Missing keyword argument 'c' for format string -E: 15:pprint: Not enough arguments for format string -E: 16:pprint: Missing keyword argument 'a' for format string -E: 16:pprint: Missing keyword argument 'b' for format string -W: 13:pprint: Unused format argument 'c' \ No newline at end of file +E: 29:pprint: Too many arguments for format string +E: 30:pprint: Invalid Python 3 format string +E: 31:pprint: Invalid Python 3 format string +E: 34:pprint: Missing keyword argument 'a' for format string +E: 35:pprint: Missing keyword argument 'b' for format string +E: 36:pprint: Missing keyword argument 'a' for format string +E: 37:pprint: Not enough arguments for format string +E: 38:pprint: Too many arguments for format string +E: 39:pprint: Missing keyword argument 'a' for format string +E: 39:pprint: Missing keyword argument 'b' for format string +E: 39:pprint: Missing keyword argument 'c' for format string +E: 40:pprint: Not enough arguments for format string +E: 41:pprint: Missing keyword argument 'a' for format string +E: 41:pprint: Missing keyword argument 'b' for format string +E: 61:pprint: Missing keyword argument 'b' for format string +W: 32:pprint: Format string contains both automatic field numbering and manual field specification +W: 35:pprint: Unused format argument 'c' +W: 48:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' +W: 56:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' +W: 57:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +W: 61:pprint: Unused format argument 'a' \ No newline at end of file -- cgit v1.2.1 From cd4fc378a3313a16e175e482ae71864cffd17f00 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 8 Jun 2014 10:16:06 +0300 Subject: Make sure that split_format_field_names always returns ints instead of longs for Python 2. --- checkers/strings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/checkers/strings.py b/checkers/strings.py index 6859ef5..4f6ff1d 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -117,8 +117,18 @@ if _PY3K: def split_format_field_names(format_string): return _string.formatter_field_name_split(format_string) else: + def _field_iterator_convertor(iterator): + for is_attr, key in iterator: + if not isinstance(key, str): + yield is_attr, int(key) + else: + yield is_attr, key + def split_format_field_names(format_string): - return format_string._formatter_field_name_split() + keyname, fielditerator = format_string._formatter_field_name_split() + # it will return longs, instead of ints, which will complicate + # the output + return keyname, _field_iterator_convertor(fielditerator) def parse_format_method_string(format_string): """ -- cgit v1.2.1 From c4b02eb401627909b3f0b113f652256010b501bb Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 8 Jun 2014 11:06:35 +0300 Subject: Small fixes. --- checkers/strings.py | 87 +++++++++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 4f6ff1d..3900264 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -44,10 +44,8 @@ MSGS = { conversion specifier."), 'E1302': ("Mixing named and unnamed conversion specifiers in format string", "mixed-format-string", - "Used when a format string contains both named (e.g. '%(foo)d' \ - or '{foo}' for Python 3 format string) \ - and unnamed (e.g. '%d' or '{}' for Python 3 format string) \ - conversion specifiers. This is also \ + "Used when a format string contains both named (e.g. '%(foo)d') \ + and unnamed (e.g. '%d') conversion specifiers. This is also \ used when a named conversion specifier contains * for the \ minimum field width and/or precision."), 'E1303': ("Expected mapping for format string, not %s", @@ -123,26 +121,24 @@ else: yield is_attr, int(key) else: yield is_attr, key - + def split_format_field_names(format_string): keyname, fielditerator = format_string._formatter_field_name_split() # it will return longs, instead of ints, which will complicate # the output - return keyname, _field_iterator_convertor(fielditerator) + return keyname, _field_iterator_convertor(fielditerator) def parse_format_method_string(format_string): """ - Parses a Python 3 format string, returning a tuple of - (keys, num_args), - where keys is the set of mapping keys in the format string, num_args - is the number of arguments required by the format string and accessors - is a dictionary of attribute accessors like '{a.field}' or element - accessor, like '{0[2]}'. + Parses a Python 3 format string, returning a tuple of + (keys, num_args), + where keys is the set of mapping keys in the format string and num_args + is the number of arguments required by the format string. """ keys = [] num_args = 0 formatter = string.Formatter() - parseiterator = formatter.parse(format_string) + parseiterator = formatter.parse(format_string) try: for result in parseiterator: if all(item is None for item in result[1:]): @@ -172,7 +168,7 @@ def get_args(callfunc): """ positional = 0 named = {} - + for arg in callfunc.args: if isinstance(arg, astroid.Keyword): named[arg.arg] = utils.safe_infer(arg.value) @@ -184,17 +180,16 @@ def get_access_path(key, parts): """ Given a list of format specifiers, returns the final access path (e.g. a.b.c[0][1]) """ - parts = parts[:] - parts.reverse() + parts = deque(parts[:]) path = [] while parts: - is_attribute, specifier = parts.pop() + is_attribute, specifier = parts.popleft() if is_attribute: - path.append(".{}".format(specifier)) + path.append(".{}".format(specifier)) else: - path.append("[{!r}]".format(specifier)) - return key + "".join(path) - + path.append("[{!r}]".format(specifier)) + return key + "".join(path) + class StringFormatChecker(BaseChecker): """Checks string formatting operations to ensure that the format string @@ -303,16 +298,16 @@ class StringMethodsChecker(BaseChecker): and isinstance(func.bound, astroid.Instance) and func.bound.name in ('str', 'unicode', 'bytes')): - if func.name in ('strip', 'lstrip', 'rstrip') and node.args: - arg = utils.safe_infer(node.args[0]) - if not isinstance(arg, astroid.Const): - return - if len(arg.value) != len(set(arg.value)): - self.add_message('bad-str-strip-call', node=node, - args=(func.bound.name, func.name)) - elif func.name == 'format': - if _PY27 or _PY3K: - self._check_new_format(node, func) + if func.name in ('strip', 'lstrip', 'rstrip') and node.args: + arg = utils.safe_infer(node.args[0]) + if not isinstance(arg, astroid.Const): + return + if len(arg.value) != len(set(arg.value)): + self.add_message('bad-str-strip-call', node=node, + args=(func.bound.name, func.name)) + elif func.name == 'format': + if _PY27 or _PY3K: + self._check_new_format(node, func) def _check_new_format(self, node, func): """ Check the new string formatting. """ @@ -342,14 +337,14 @@ class StringMethodsChecker(BaseChecker): if named_fields: for field in named_fields: if field not in named and field: - self.add_message('missing-format-argument-key', + self.add_message('missing-format-argument-key', node=node, args=(field, )) for field in named: if field not in named_fields: self.add_message('unused-format-string-argument', node=node, - args=(field, )) + args=(field, )) else: if positional > num_args: # We can have two possibilities: @@ -359,14 +354,14 @@ class StringMethodsChecker(BaseChecker): if len(manual_fields) != positional: self.add_message('too-many-format-args', node=node) elif positional < num_args: - self.add_message('too-few-format-args', node=node) - + self.add_message('too-few-format-args', node=node) + if manual_fields and positional < len(manual_fields): - self.add_message('too-few-format-args', node=node) + self.add_message('too-few-format-args', node=node) self._check_new_format_specifiers(node, fields, named) - def _check_new_format_specifiers(self, node, fields, named): + def _check_new_format_specifiers(self, node, fields, named): """ Check attribute and index access in the format string ("{0.a}" and "{0[a]}"). @@ -378,15 +373,15 @@ class StringMethodsChecker(BaseChecker): try: argument = utils.get_argument_from_call(node, key) except utils.NoSuchArgumentError: - break + continue else: if key not in named: - break + continue argument = named[key] try: argument = next(argument.infer()) except astroid.InferenceError: - break + continue if not specifiers: # No need to check this key if it doesn't # use attribute / item access @@ -394,7 +389,7 @@ class StringMethodsChecker(BaseChecker): previous = argument specifiers = deque(specifiers[:]) parsed = [] - + while specifiers: is_attribute, specifier = specifiers.popleft() parsed.append((is_attribute, specifier)) @@ -404,7 +399,7 @@ class StringMethodsChecker(BaseChecker): except astroid.NotFoundError: if (hasattr(previous, 'has_dynamic_getattr') and previous.has_dynamic_getattr()): - # Can't process further + # Don't warn if the object has a custom __getattr__ break path = get_access_path(key, parsed) self.add_message('missing-format-attribute', @@ -422,10 +417,10 @@ class StringMethodsChecker(BaseChecker): try: # Lookup __getitem__ in the current node, # but skip further checks, because we can't - # retrieve the looked object + # retrieve the looked object previous.getattr('__getitem__') break - except NotFoundError: + except astroid.NotFoundError: warn_error = True if warn_error: path = get_access_path(key, parsed) @@ -441,8 +436,8 @@ class StringMethodsChecker(BaseChecker): break if previous is astroid.YES: break - - + + class StringConstantChecker(BaseTokenChecker): """Check string literals""" __implements__ = (ITokenChecker, IRawChecker) -- cgit v1.2.1 From d639355b4240339ad6cbf3e9f63ff6f7eb000a85 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 8 Jun 2014 11:13:00 +0300 Subject: Add changelog entry and improve the tests. --- ChangeLog | 5 +++++ test/input/func_string_format_py27.py | 1 + test/messages/func_string_format_py27.txt | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 9b675d0..383099a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,11 @@ ChangeLog for Pylint -- * Fix unused-import false positive with Python 3 metaclasses (#143). + * Add multiple checks for PEP 3101 advanced string formatting: + 'bad-format-string', 'missing-format-argument-key', + 'unused-format-string-argument', 'format-combined-specification', + 'missing-format-attribute' and 'invalid-format-index'. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index 7d76662..7b056a2 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -59,3 +59,4 @@ def pprint(): print "{0.missing.length}".format(ReturnYes()) print "{1.missing.length}".format(ReturnYes()) print "{b[0]}".format(a=23) + print "{a[0]}".format(a=object) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index 22f072d..354cd18 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -18,4 +18,5 @@ W: 35:pprint: Unused format argument 'c' W: 48:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' W: 56:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' W: 57:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' -W: 61:pprint: Unused format argument 'a' \ No newline at end of file +W: 61:pprint: Unused format argument 'a' +W: 62:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file -- cgit v1.2.1 From c595776620459e774d3c245a05e8058142cd6e36 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 8 Jun 2014 11:32:35 +0300 Subject: Change message ids. --- checkers/strings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 3900264..202dfb0 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -75,30 +75,30 @@ MSGS = { "Used when a format string that uses unnamed conversion \ specifiers is given too few arguments"), - 'E1307': ("Invalid Python 3 format string", + 'W1302': ("Invalid Python 3 format string", "bad-format-string", "Used when a Python 3 format string is invalid"), - 'E1308': ("Missing keyword argument %r for format string", + 'W1303': ("Missing keyword argument %r for format string", "missing-format-argument-key", "Used when a Python 3 format string that uses named fields \ doesn't receive one or more required keywords."), - 'W1302': ("Unused format argument %r", + 'W1304': ("Unused format argument %r", "unused-format-string-argument", "Used when a Python 3 format string that uses named \ fields is used with an argument that \ is not required by the format string."), - 'W1303': ("Format string contains both automatic field numbering " + 'W1305': ("Format string contains both automatic field numbering " "and manual field specification", "format-combined-specification", "Usen when a format string contains both automatic \ field numbering (e.g. '{}') and manual field \ specification (e.g. '{0}')"), - 'W1304': ("Missing format attribute %r in format specifier %r", + 'W1306': ("Missing format attribute %r in format specifier %r", "missing-format-attribute", "Used when a format string uses an attribute specifier " "({0.length}), but the argument passed for formatting " "doesn't have that attribute."), - 'W1305': ("Using invalid lookup key %r in format specifier %r", + 'W1307': ("Using invalid lookup key %r in format specifier %r", "invalid-format-index", "Used when a format string uses a lookup specifier " "({a[1]}), but the argument passed for formatting " -- cgit v1.2.1 From 0cb1b2077c77f68b220c55f1ffa469d24571638a Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 8 Jun 2014 11:56:33 +0300 Subject: Fix expected output. --- test/messages/func_string_format_py27.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index 354cd18..4f45bca 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -1,22 +1,22 @@ E: 29:pprint: Too many arguments for format string -E: 30:pprint: Invalid Python 3 format string -E: 31:pprint: Invalid Python 3 format string -E: 34:pprint: Missing keyword argument 'a' for format string -E: 35:pprint: Missing keyword argument 'b' for format string -E: 36:pprint: Missing keyword argument 'a' for format string E: 37:pprint: Not enough arguments for format string E: 38:pprint: Too many arguments for format string -E: 39:pprint: Missing keyword argument 'a' for format string -E: 39:pprint: Missing keyword argument 'b' for format string -E: 39:pprint: Missing keyword argument 'c' for format string E: 40:pprint: Not enough arguments for format string -E: 41:pprint: Missing keyword argument 'a' for format string -E: 41:pprint: Missing keyword argument 'b' for format string -E: 61:pprint: Missing keyword argument 'b' for format string +W: 30:pprint: Invalid Python 3 format string +W: 31:pprint: Invalid Python 3 format string W: 32:pprint: Format string contains both automatic field numbering and manual field specification +W: 34:pprint: Missing keyword argument 'a' for format string +W: 35:pprint: Missing keyword argument 'b' for format string W: 35:pprint: Unused format argument 'c' +W: 36:pprint: Missing keyword argument 'a' for format string +W: 39:pprint: Missing keyword argument 'a' for format string +W: 39:pprint: Missing keyword argument 'b' for format string +W: 39:pprint: Missing keyword argument 'c' for format string +W: 41:pprint: Missing keyword argument 'a' for format string +W: 41:pprint: Missing keyword argument 'b' for format string W: 48:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' W: 56:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' W: 57:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +W: 61:pprint: Missing keyword argument 'b' for format string W: 61:pprint: Unused format argument 'a' W: 62:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file -- cgit v1.2.1 From e8d8cef9c6a455599f30bd60693b1279fb127e58 Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 13 Jun 2014 14:23:31 +0300 Subject: Skip YES arguments, add a few test cases. --- checkers/strings.py | 2 ++ test/input/func_string_format_py27.py | 8 +++++- test/messages/func_string_format_py27.txt | 43 ++++++++++++++++--------------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 202dfb0..3735756 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -378,6 +378,8 @@ class StringMethodsChecker(BaseChecker): if key not in named: continue argument = named[key] + if argument is astroid.YES: + continue try: argument = next(argument.infer()) except astroid.InferenceError: diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index 7b056a2..2eba807 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -1,6 +1,6 @@ """test for Python 3 string formatting error """ -# pylint: disable=too-few-public-methods, import-error +# pylint: disable=too-few-public-methods, import-error, unused-argument from missing import Missing __revision__ = 1 @@ -24,6 +24,10 @@ class ReturnYes(object): """ can't be properly infered """ missing = Missing() +def log(message, message_type="error"): + """ Test """ + return message + def pprint(): """Test string format """ print "{{}}".format(1) @@ -60,3 +64,5 @@ def pprint(): print "{1.missing.length}".format(ReturnYes()) print "{b[0]}".format(a=23) print "{a[0]}".format(a=object) + print "{a}".format(a=Missing()) + print log("{}".format(2, "info")) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index 4f45bca..6b71517 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -1,22 +1,23 @@ -E: 29:pprint: Too many arguments for format string -E: 37:pprint: Not enough arguments for format string -E: 38:pprint: Too many arguments for format string -E: 40:pprint: Not enough arguments for format string -W: 30:pprint: Invalid Python 3 format string -W: 31:pprint: Invalid Python 3 format string -W: 32:pprint: Format string contains both automatic field numbering and manual field specification -W: 34:pprint: Missing keyword argument 'a' for format string -W: 35:pprint: Missing keyword argument 'b' for format string -W: 35:pprint: Unused format argument 'c' -W: 36:pprint: Missing keyword argument 'a' for format string -W: 39:pprint: Missing keyword argument 'a' for format string +E: 33:pprint: Too many arguments for format string +E: 41:pprint: Not enough arguments for format string +E: 42:pprint: Too many arguments for format string +E: 44:pprint: Not enough arguments for format string +E: 68:pprint: Too many arguments for format string +W: 34:pprint: Invalid Python 3 format string +W: 35:pprint: Invalid Python 3 format string +W: 36:pprint: Format string contains both automatic field numbering and manual field specification +W: 38:pprint: Missing keyword argument 'a' for format string W: 39:pprint: Missing keyword argument 'b' for format string -W: 39:pprint: Missing keyword argument 'c' for format string -W: 41:pprint: Missing keyword argument 'a' for format string -W: 41:pprint: Missing keyword argument 'b' for format string -W: 48:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' -W: 56:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' -W: 57:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' -W: 61:pprint: Missing keyword argument 'b' for format string -W: 61:pprint: Unused format argument 'a' -W: 62:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file +W: 39:pprint: Unused format argument 'c' +W: 40:pprint: Missing keyword argument 'a' for format string +W: 43:pprint: Missing keyword argument 'a' for format string +W: 43:pprint: Missing keyword argument 'b' for format string +W: 43:pprint: Missing keyword argument 'c' for format string +W: 45:pprint: Missing keyword argument 'a' for format string +W: 45:pprint: Missing keyword argument 'b' for format string +W: 52:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' +W: 60:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' +W: 61:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +W: 65:pprint: Missing keyword argument 'b' for format string +W: 65:pprint: Unused format argument 'a' +W: 66:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file -- cgit v1.2.1 From 0298f143249c31f627f0b84b5844d99dffc03ee9 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 14 Jun 2014 23:35:41 +0300 Subject: Move the filtering of YES nodes at the beginning of while block. --- checkers/strings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 3735756..25f2f00 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -393,6 +393,8 @@ class StringMethodsChecker(BaseChecker): parsed = [] while specifiers: + if previous is astroid.YES: + break is_attribute, specifier = specifiers.popleft() parsed.append((is_attribute, specifier)) if is_attribute: @@ -436,8 +438,7 @@ class StringMethodsChecker(BaseChecker): except astroid.InferenceError: # can't check further if we can't infer it break - if previous is astroid.YES: - break + class StringConstantChecker(BaseTokenChecker): -- cgit v1.2.1 From c86f5196aa899651dbf6667e86c7d3112def7159 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 15 Jun 2014 01:56:09 +0300 Subject: Skip uninferable nodes when looking for bad exception contexts --- checkers/exceptions.py | 2 ++ test/input/func_bad_exception_context_py30.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 84f92ea..7e0f3fc 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -153,6 +153,8 @@ class ExceptionsChecker(BaseChecker): except astroid.InferenceError: pass else: + if cause is YES: + return if isinstance(cause, astroid.Const): if cause.value is not None: self.add_message('bad-exception-context', diff --git a/test/input/func_bad_exception_context_py30.py b/test/input/func_bad_exception_context_py30.py index d3ab127..98b44ee 100644 --- a/test/input/func_bad_exception_context_py30.py +++ b/test/input/func_bad_exception_context_py30.py @@ -1,8 +1,8 @@ """Check that raise ... from .. uses a proper exception context """ -# pylint: disable=unreachable +# pylint: disable=unreachable, import-error -import socket +import socket, unknown __revision__ = 0 @@ -21,3 +21,4 @@ def test(): raise IndexError() from ZeroDivisionError raise IndexError() from ZeroDivisionError() raise IndexError() from object() + raise IndexError() from unknown -- cgit v1.2.1 From 2f173114dfd45952daa34f2eb5912e8914d8fdb7 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 15 Jun 2014 02:14:22 +0300 Subject: Filter YES arguments. --- checkers/strings.py | 4 +-- test/input/func_string_format_py27.py | 2 ++ test/messages/func_string_format_py27.txt | 44 +++++++++++++++---------------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 25f2f00..4f4293a 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -378,13 +378,13 @@ class StringMethodsChecker(BaseChecker): if key not in named: continue argument = named[key] - if argument is astroid.YES: + if argument in (astroid.YES, None): continue try: argument = next(argument.infer()) except astroid.InferenceError: continue - if not specifiers: + if not specifiers or argument is astroid.YES: # No need to check this key if it doesn't # use attribute / item access continue diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index 2eba807..cd3071b 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -1,6 +1,7 @@ """test for Python 3 string formatting error """ # pylint: disable=too-few-public-methods, import-error, unused-argument +import os from missing import Missing __revision__ = 1 @@ -66,3 +67,4 @@ def pprint(): print "{a[0]}".format(a=object) print "{a}".format(a=Missing()) print log("{}".format(2, "info")) + print "{pid}".format(pid=os.getpid()) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index 6b71517..632ff60 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -1,23 +1,23 @@ -E: 33:pprint: Too many arguments for format string -E: 41:pprint: Not enough arguments for format string -E: 42:pprint: Too many arguments for format string -E: 44:pprint: Not enough arguments for format string -E: 68:pprint: Too many arguments for format string -W: 34:pprint: Invalid Python 3 format string +E: 34:pprint: Too many arguments for format string +E: 42:pprint: Not enough arguments for format string +E: 43:pprint: Too many arguments for format string +E: 45:pprint: Not enough arguments for format string +E: 69:pprint: Too many arguments for format string W: 35:pprint: Invalid Python 3 format string -W: 36:pprint: Format string contains both automatic field numbering and manual field specification -W: 38:pprint: Missing keyword argument 'a' for format string -W: 39:pprint: Missing keyword argument 'b' for format string -W: 39:pprint: Unused format argument 'c' -W: 40:pprint: Missing keyword argument 'a' for format string -W: 43:pprint: Missing keyword argument 'a' for format string -W: 43:pprint: Missing keyword argument 'b' for format string -W: 43:pprint: Missing keyword argument 'c' for format string -W: 45:pprint: Missing keyword argument 'a' for format string -W: 45:pprint: Missing keyword argument 'b' for format string -W: 52:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' -W: 60:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' -W: 61:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' -W: 65:pprint: Missing keyword argument 'b' for format string -W: 65:pprint: Unused format argument 'a' -W: 66:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file +W: 36:pprint: Invalid Python 3 format string +W: 37:pprint: Format string contains both automatic field numbering and manual field specification +W: 39:pprint: Missing keyword argument 'a' for format string +W: 40:pprint: Missing keyword argument 'b' for format string +W: 40:pprint: Unused format argument 'c' +W: 41:pprint: Missing keyword argument 'a' for format string +W: 44:pprint: Missing keyword argument 'a' for format string +W: 44:pprint: Missing keyword argument 'b' for format string +W: 44:pprint: Missing keyword argument 'c' for format string +W: 46:pprint: Missing keyword argument 'a' for format string +W: 46:pprint: Missing keyword argument 'b' for format string +W: 53:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' +W: 61:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' +W: 62:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +W: 66:pprint: Missing keyword argument 'b' for format string +W: 66:pprint: Unused format argument 'a' +W: 67:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file -- cgit v1.2.1 From e7ee2c6c0930701ed6efa7efa9f14ff7764c3de1 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 15 Jun 2014 09:25:53 +0300 Subject: Fix a potential crash for accessors like "{0.missing}". --- checkers/strings.py | 2 +- test/input/func_string_format_py27.py | 1 + test/messages/func_string_format_py27.txt | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 4f4293a..9be6627 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -188,7 +188,7 @@ def get_access_path(key, parts): path.append(".{}".format(specifier)) else: path.append("[{!r}]".format(specifier)) - return key + "".join(path) + return str(key) + "".join(path) class StringFormatChecker(BaseChecker): diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index cd3071b..a492bb2 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -68,3 +68,4 @@ def pprint(): print "{a}".format(a=Missing()) print log("{}".format(2, "info")) print "{pid}".format(pid=os.getpid()) + print "{0.missing}".format(2) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index 632ff60..53edb8b 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -20,4 +20,5 @@ W: 61:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' W: 62:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' W: 66:pprint: Missing keyword argument 'b' for format string W: 66:pprint: Unused format argument 'a' -W: 67:pprint: Using invalid lookup key 0 in format specifier 'a[0]' \ No newline at end of file +W: 67:pprint: Using invalid lookup key 0 in format specifier 'a[0]' +W: 71:pprint: Missing format attribute 'missing' in format specifier '0.missing' \ No newline at end of file -- cgit v1.2.1 From 6d01a837357d6c281f30ad30bc6d3183078c3b92 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 15 Jun 2014 12:07:24 +0300 Subject: Filter out non-const nodes. --- checkers/strings.py | 2 ++ test/input/func_string_format_py27.py | 1 + 2 files changed, 3 insertions(+) diff --git a/checkers/strings.py b/checkers/strings.py index 9be6627..2d8dce6 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -319,6 +319,8 @@ class StringMethodsChecker(BaseChecker): positional, named = get_args(node) except astroid.InferenceError: return + if not isinstance(strnode, astroid.Const): + return try: fields, num_args = parse_format_method_string(strnode.value) except utils.IncompleteFormatString: diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index a492bb2..458f462 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -69,3 +69,4 @@ def pprint(): print log("{}".format(2, "info")) print "{pid}".format(pid=os.getpid()) print "{0.missing}".format(2) + print str("{}").format(2) -- cgit v1.2.1 From d8095acce29d9d77f7e00b4be4d1b6aaf9a6c7c3 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 16 Jun 2014 12:55:49 +0300 Subject: Skip nodes with starargs or kwargs for now. --- checkers/strings.py | 4 ++++ test/input/func_string_format_py27.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/checkers/strings.py b/checkers/strings.py index 2d8dce6..d5a312c 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -315,6 +315,10 @@ class StringMethodsChecker(BaseChecker): strnode = func.bound.infer().next() except astroid.InferenceError: return + if node.starargs or node.kwargs: + # Don't complicate the logic, skip + # these for now. + return try: positional, named = get_args(node) except astroid.InferenceError: diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index 458f462..868355f 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -1,6 +1,6 @@ """test for Python 3 string formatting error """ -# pylint: disable=too-few-public-methods, import-error, unused-argument +# pylint: disable=too-few-public-methods, import-error, unused-argument, star-args import os from missing import Missing @@ -70,3 +70,6 @@ def pprint(): print "{pid}".format(pid=os.getpid()) print "{0.missing}".format(2) print str("{}").format(2) + # these are skipped + print "{0} {1}".format(*[1, 2]) + print "{a} {b}".format(**{'a': 1, 'b': 2}) -- cgit v1.2.1 From c5e05b7c2e3079250a5e3b274b34aec5bf4c2aa9 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 16 Jun 2014 22:57:57 +0300 Subject: Don't warn with 'bad-format-character' when encountering the 'a' format on Python 3. --- ChangeLog | 3 +++ checkers/utils.py | 8 +++++++- test/input/func_e12xx.py | 2 +- test/input/func_e13xx.py | 1 + test/messages/func_e12xx.txt | 2 +- test/messages/func_e13xx.txt | 1 + test/messages/func_e13xx_py30.txt | 11 +++++++++++ 7 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 test/messages/func_e13xx_py30.txt diff --git a/ChangeLog b/ChangeLog index 9b675d0..6f780b8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -4,6 +4,9 @@ ChangeLog for Pylint -- * Fix unused-import false positive with Python 3 metaclasses (#143). + * Don't warn with 'bad-format-character' when encountering + the 'a' format on Python 3. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/utils.py b/checkers/utils.py index e7d85d4..6f40538 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -19,6 +19,7 @@ """ import re +import sys import string import astroid @@ -28,6 +29,7 @@ from logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ COMP_NODE_TYPES = astroid.ListComp, astroid.SetComp, astroid.DictComp, astroid.GenExpr +PY3K = sys.version_info[0] == 3 class NoSuchArgumentError(Exception): @@ -345,7 +347,11 @@ def parse_format_string(format_string): if char in 'hlL': i, char = next_char(i) # Parse the conversion type (mandatory). - if char not in 'diouxXeEfFgGcrs%': + if PY3K: + flags = 'diouxXeEfFgGcrs%a' + else: + flags = 'diouxXeEfFgGcrs%' + if char not in flags: raise UnsupportedFormatCharacter(i) if key: keys.add(key) diff --git a/test/input/func_e12xx.py b/test/input/func_e12xx.py index 6482c92..83df7ab 100644 --- a/test/input/func_e12xx.py +++ b/test/input/func_e12xx.py @@ -14,7 +14,7 @@ def pprint(): logging.info('', '') # 1205 logging.info('%s%', '') # 1201 logging.info('%s%s', '') # 1206 - logging.info('%s%a', '', '') # 1200 + logging.info('%s%y', '', '') # 1200 logging.info('%s%s', '', '', '') # 1205 # These should be okay: diff --git a/test/input/func_e13xx.py b/test/input/func_e13xx.py index a0d39ef..1eaf598 100644 --- a/test/input/func_e13xx.py +++ b/test/input/func_e13xx.py @@ -18,4 +18,5 @@ def pprint(): print "%(PARG_1)d %(PARG_2)d" % [2, 3] # 1303 print "%2z" % PARG_1 print "strange format %2" % PARG_2 + print "works in 3 %a" % 1 diff --git a/test/messages/func_e12xx.txt b/test/messages/func_e12xx.txt index 690e6f4..d0a8b9c 100644 --- a/test/messages/func_e12xx.txt +++ b/test/messages/func_e12xx.txt @@ -2,5 +2,5 @@ E: 13:pprint: Too many arguments for logging format string E: 14:pprint: Too many arguments for logging format string E: 15:pprint: Logging format string ends in middle of conversion specifier E: 16:pprint: Not enough arguments for logging format string -E: 17:pprint: Unsupported logging format character 'a' (0x61) at index 3 +E: 17:pprint: Unsupported logging format character 'y' (0x79) at index 3 E: 18:pprint: Too many arguments for logging format string diff --git a/test/messages/func_e13xx.txt b/test/messages/func_e13xx.txt index c130949..f2d0d36 100644 --- a/test/messages/func_e13xx.txt +++ b/test/messages/func_e13xx.txt @@ -7,6 +7,7 @@ E: 17:pprint: Expected mapping for format string, not Tuple E: 18:pprint: Expected mapping for format string, not List E: 19:pprint: Unsupported format character 'z' (0x7a) at index 2 E: 20:pprint: Format string ends in middle of conversion specifier +E: 21:pprint: Unsupported format character 'a' (0x61) at index 12 W: 15:pprint: Unused key 'PARG_3' in format string dictionary W: 16:pprint: Format string dictionary key should be a string, not 2 diff --git a/test/messages/func_e13xx_py30.txt b/test/messages/func_e13xx_py30.txt new file mode 100644 index 0000000..7ac9fb1 --- /dev/null +++ b/test/messages/func_e13xx_py30.txt @@ -0,0 +1,11 @@ +E: 11:pprint: Not enough arguments for format string +E: 12:pprint: Too many arguments for format string +E: 13:pprint: Mixing named and unnamed conversion specifiers in format string +E: 14:pprint: Missing key 'PARG_2' in format string dictionary +E: 16:pprint: Missing key 'PARG_2' in format string dictionary +E: 17:pprint: Expected mapping for format string, not Tuple +E: 18:pprint: Expected mapping for format string, not List +E: 19:pprint: Unsupported format character 'z' (0x7a) at index 2 +E: 20:pprint: Format string ends in middle of conversion specifier +W: 15:pprint: Unused key 'PARG_3' in format string dictionary +W: 16:pprint: Format string dictionary key should be a string, not 2 \ No newline at end of file -- cgit v1.2.1 From 8802015048ce815bca56dc185e95b6e174021703 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 23 Jun 2014 10:24:47 +0300 Subject: No need for a deque here. --- checkers/strings.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index d5a312c..a15206d 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -21,7 +21,6 @@ import sys import tokenize import string -from collections import deque import astroid @@ -180,10 +179,8 @@ def get_access_path(key, parts): """ Given a list of format specifiers, returns the final access path (e.g. a.b.c[0][1]) """ - parts = deque(parts[:]) path = [] - while parts: - is_attribute, specifier = parts.popleft() + for is_attribute, specifier in parts: if is_attribute: path.append(".{}".format(specifier)) else: @@ -394,14 +391,12 @@ class StringMethodsChecker(BaseChecker): # No need to check this key if it doesn't # use attribute / item access continue + previous = argument - specifiers = deque(specifiers[:]) parsed = [] - - while specifiers: + for is_attribute, specifier in specifiers: if previous is astroid.YES: break - is_attribute, specifier = specifiers.popleft() parsed.append((is_attribute, specifier)) if is_attribute: try: -- cgit v1.2.1 From 07053ad381c1eda8ba17332134d7018c6c449eb8 Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 26 Jun 2014 05:55:59 +0300 Subject: Separate the good cases from the bad cases, improve the messages. --- checkers/strings.py | 39 ++++++++++++----------- test/input/func_string_format_py27.py | 51 ++++++++++++++++--------------- test/messages/func_string_format_py27.txt | 50 +++++++++++++++--------------- 3 files changed, 75 insertions(+), 65 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index a15206d..b04d1c6 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -74,34 +74,34 @@ MSGS = { "Used when a format string that uses unnamed conversion \ specifiers is given too few arguments"), - 'W1302': ("Invalid Python 3 format string", + 'W1302': ("Invalid format string", "bad-format-string", - "Used when a Python 3 format string is invalid"), + "Used when a PEP 3101 format string is invalid"), 'W1303': ("Missing keyword argument %r for format string", "missing-format-argument-key", - "Used when a Python 3 format string that uses named fields \ - doesn't receive one or more required keywords."), + "Used when a PEP 3101 format string that uses named fields " + "doesn't receive one or more required keywords."), 'W1304': ("Unused format argument %r", "unused-format-string-argument", - "Used when a Python 3 format string that uses named \ - fields is used with an argument that \ - is not required by the format string."), + "Used when a PEP 3101 format string that uses named " + "fields is used with an argument that " + "is not required by the format string."), 'W1305': ("Format string contains both automatic field numbering " "and manual field specification", "format-combined-specification", - "Usen when a format string contains both automatic \ - field numbering (e.g. '{}') and manual field \ - specification (e.g. '{0}')"), + "Usen when a PEP 3101 format string contains both automatic " + "field numbering (e.g. '{}') and manual field " + "specification (e.g. '{0}')"), 'W1306': ("Missing format attribute %r in format specifier %r", "missing-format-attribute", - "Used when a format string uses an attribute specifier " - "({0.length}), but the argument passed for formatting " - "doesn't have that attribute."), + "Used when a PEP 3101 format string uses an " + "attribute specifier ({0.length}), but the argument " + "passed for formatting doesn't have that attribute."), 'W1307': ("Using invalid lookup key %r in format specifier %r", "invalid-format-index", - "Used when a format string uses a lookup specifier " + "Used when a PEP 3101 format string uses a lookup specifier " "({a[1]}), but the argument passed for formatting " - "doesn't contain that key.") + "doesn't contain or doesn't have that key as an attribute.") } OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, @@ -372,6 +372,11 @@ class StringMethodsChecker(BaseChecker): for key, specifiers in fields: # Obtain the argument. If it can't be obtained # or infered, skip this check. + if key == '': + # {[0]} will have an unnamed argument, defaulting + # to 0. It will not be present in `named`, so use the value + # 0 for it. + key = 0 if isinstance(key, int): try: argument = utils.get_argument_from_call(node, key) @@ -384,7 +389,7 @@ class StringMethodsChecker(BaseChecker): if argument in (astroid.YES, None): continue try: - argument = next(argument.infer()) + argument = argument.infer().next() except astroid.InferenceError: continue if not specifiers or argument is astroid.YES: @@ -435,7 +440,7 @@ class StringMethodsChecker(BaseChecker): break try: - previous = next(previous.infer()) + previous = previous.infer().next() except astroid.InferenceError: # can't check further if we can't infer it break diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py index 868355f..d2f5599 100644 --- a/test/input/func_string_format_py27.py +++ b/test/input/func_string_format_py27.py @@ -29,14 +29,36 @@ def log(message, message_type="error"): """ Test """ return message -def pprint(): +def print_good(): + """ Good format strings """ + print "{0} {1}".format(1, 2) + print "{0!r:20}".format("Hello") + print "{!r:20}".format("Hello") + print "{a!r:20}".format(a="Hello") + print "{pid}".format(pid=os.getpid()) + print str("{}").format(2) + print "{0.missing.length}".format(ReturnYes()) + print "{1.missing.length}".format(ReturnYes()) + print "{a.ids[3][1]}".format(a=Test()) + print "{a[0][0]}".format(a=[[1]]) + print "{[0][0]}".format({0: {0: 1}}) + print "{a.test}".format(a=Custom()) + print "{a.__len__}".format(a=[]) + print "{a.ids.__len__}".format(a=Test()) + print "{a[0]}".format(a=Getitem()) + print "{a[0][0]}".format(a=[Getitem()]) + print "{[0][0]}".format(["test"]) + # these are skipped + print "{0} {1}".format(*[1, 2]) + print "{a} {b}".format(**{'a': 1, 'b': 2}) + print "{a}".format(a=Missing()) + +def pprint_bad(): """Test string format """ print "{{}}".format(1) print "{} {".format() print "{} }".format() print "{0} {}".format(1, 2) - print "{0} {1}".format(1, 2) - print "{0} {1} {a}".format(1, 2, 3) print "{a} {b}".format(a=1, c=2) print "{} {a}".format(1, 2) print "{} {}".format(1) @@ -44,32 +66,13 @@ def pprint(): print "{a} {b} {c}".format() print "{} {}".format(a=1, b=2) print "{a} {b}".format(1, 2) - print "{0!r:20}".format("Hello") - print "{!r:20}".format("Hello") - print "{a!r:20}".format(a="Hello") - print "{a.test}".format(a=Custom()) - print "{a.__len__}".format(a=[]) - print "{a.ids.__len__}".format(a=Test()) + print "{0} {1} {a}".format(1, 2, 3) print "{a.ids.__len__.length}".format(a=Test()) - print "{a[0]}".format(a=Getitem()) - print "{a[0][0]}".format(a=[Getitem()]) - print "{a[0][0]}".format(a=[[1]]) - print "{[0][0]}".format(((1, ))) - print "{[0][0]}".format({0: {0: 1}}) - print "{[0][0]}".format(["test"]) - print "{a.ids[3][1]}".format(a=Test()) print "{a.ids[3][400]}".format(a=Test()) print "{a.ids[3]['string']}".format(a=Test()) print "{[0][1]}".format(["a"]) - print "{0.missing.length}".format(ReturnYes()) - print "{1.missing.length}".format(ReturnYes()) + print "{[0][0]}".format(((1, ))) print "{b[0]}".format(a=23) print "{a[0]}".format(a=object) - print "{a}".format(a=Missing()) print log("{}".format(2, "info")) - print "{pid}".format(pid=os.getpid()) print "{0.missing}".format(2) - print str("{}").format(2) - # these are skipped - print "{0} {1}".format(*[1, 2]) - print "{a} {b}".format(**{'a': 1, 'b': 2}) diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt index 53edb8b..56f8c41 100644 --- a/test/messages/func_string_format_py27.txt +++ b/test/messages/func_string_format_py27.txt @@ -1,24 +1,26 @@ -E: 34:pprint: Too many arguments for format string -E: 42:pprint: Not enough arguments for format string -E: 43:pprint: Too many arguments for format string -E: 45:pprint: Not enough arguments for format string -E: 69:pprint: Too many arguments for format string -W: 35:pprint: Invalid Python 3 format string -W: 36:pprint: Invalid Python 3 format string -W: 37:pprint: Format string contains both automatic field numbering and manual field specification -W: 39:pprint: Missing keyword argument 'a' for format string -W: 40:pprint: Missing keyword argument 'b' for format string -W: 40:pprint: Unused format argument 'c' -W: 41:pprint: Missing keyword argument 'a' for format string -W: 44:pprint: Missing keyword argument 'a' for format string -W: 44:pprint: Missing keyword argument 'b' for format string -W: 44:pprint: Missing keyword argument 'c' for format string -W: 46:pprint: Missing keyword argument 'a' for format string -W: 46:pprint: Missing keyword argument 'b' for format string -W: 53:pprint: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' -W: 61:pprint: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' -W: 62:pprint: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' -W: 66:pprint: Missing keyword argument 'b' for format string -W: 66:pprint: Unused format argument 'a' -W: 67:pprint: Using invalid lookup key 0 in format specifier 'a[0]' -W: 71:pprint: Missing format attribute 'missing' in format specifier '0.missing' \ No newline at end of file +E: 58:pprint_bad: Too many arguments for format string +E: 64:pprint_bad: Not enough arguments for format string +E: 65:pprint_bad: Too many arguments for format string +E: 67:pprint_bad: Not enough arguments for format string +E: 77:pprint_bad: Too many arguments for format string +W: 59:pprint_bad: Invalid format string +W: 60:pprint_bad: Invalid format string +W: 61:pprint_bad: Format string contains both automatic field numbering and manual field specification +W: 62:pprint_bad: Missing keyword argument 'b' for format string +W: 62:pprint_bad: Unused format argument 'c' +W: 63:pprint_bad: Missing keyword argument 'a' for format string +W: 66:pprint_bad: Missing keyword argument 'a' for format string +W: 66:pprint_bad: Missing keyword argument 'b' for format string +W: 66:pprint_bad: Missing keyword argument 'c' for format string +W: 68:pprint_bad: Missing keyword argument 'a' for format string +W: 68:pprint_bad: Missing keyword argument 'b' for format string +W: 69:pprint_bad: Missing keyword argument 'a' for format string +W: 70:pprint_bad: Missing format attribute 'length' in format specifier 'a.ids.__len__.length' +W: 71:pprint_bad: Using invalid lookup key 400 in format specifier 'a.ids[3][400]' +W: 72:pprint_bad: Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +W: 73:pprint_bad: Using invalid lookup key 1 in format specifier '0[0][1]' +W: 74:pprint_bad: Using invalid lookup key 0 in format specifier '0[0][0]' +W: 75:pprint_bad: Missing keyword argument 'b' for format string +W: 75:pprint_bad: Unused format argument 'a' +W: 76:pprint_bad: Using invalid lookup key 0 in format specifier 'a[0]' +W: 78:pprint_bad: Missing format attribute 'missing' in format specifier '0.missing' \ No newline at end of file -- cgit v1.2.1 From 75c6ce0f914f431b28c928f7b5882c2918096f7d Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 26 Jun 2014 10:36:53 +0300 Subject: Reorder a check for better coherence, fix a couple of docstrings. --- checkers/strings.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index b04d1c6..4fe16dd 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -76,7 +76,7 @@ MSGS = { 'W1302': ("Invalid format string", "bad-format-string", - "Used when a PEP 3101 format string is invalid"), + "Used when a PEP 3101 format string is invalid."), 'W1303': ("Missing keyword argument %r for format string", "missing-format-argument-key", "Used when a PEP 3101 format string that uses named fields " @@ -91,7 +91,7 @@ MSGS = { "format-combined-specification", "Usen when a PEP 3101 format string contains both automatic " "field numbering (e.g. '{}') and manual field " - "specification (e.g. '{0}')"), + "specification (e.g. '{0}')."), 'W1306': ("Missing format attribute %r in format specifier %r", "missing-format-attribute", "Used when a PEP 3101 format string uses an " @@ -129,7 +129,7 @@ else: def parse_format_method_string(format_string): """ - Parses a Python 3 format string, returning a tuple of + Parses a PEP 3101 format string, returning a tuple of (keys, num_args), where keys is the set of mapping keys in the format string and num_args is the number of arguments required by the format string. @@ -163,7 +163,7 @@ def get_args(callfunc): """ Get the arguments from the given `CallFunc` node. Return a tuple, where the first element is the number of positional arguments and the second element - is the keyword arguments in a dict + is the keyword arguments in a dict. """ positional = 0 named = {} @@ -177,7 +177,7 @@ def get_args(callfunc): def get_access_path(key, parts): """ Given a list of format specifiers, returns - the final access path (e.g. a.b.c[0][1]) + the final access path (e.g. a.b.c[0][1]). """ path = [] for is_attribute, specifier in parts: @@ -312,16 +312,15 @@ class StringMethodsChecker(BaseChecker): strnode = func.bound.infer().next() except astroid.InferenceError: return + if not isinstance(strnode, astroid.Const): + return if node.starargs or node.kwargs: - # Don't complicate the logic, skip - # these for now. + # TODO: Don't complicate the logic, skip these for now. return try: positional, named = get_args(node) except astroid.InferenceError: return - if not isinstance(strnode, astroid.Const): - return try: fields, num_args = parse_format_method_string(strnode.value) except utils.IncompleteFormatString: -- cgit v1.2.1 From 98c2677a774760eb8ad720eebc7bb6563dbc0a85 Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 27 Jun 2014 19:18:26 +0300 Subject: Issue broad-except and bare-except even if the number of except handlers is different than 1. Closes issue #113. --- ChangeLog | 3 +++ checkers/exceptions.py | 8 ++++---- test/input/func_block_disable_msg.py | 2 +- test/input/func_format_py27.py | 2 +- test/input/func_format_py_27.py | 2 +- test/messages/func_w0702.txt | 1 + test/messages/func_w0705.txt | 10 ++++++++++ 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index db2d59c..0c6a85c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -12,6 +12,9 @@ ChangeLog for Pylint 'unused-format-string-argument', 'format-combined-specification', 'missing-format-attribute' and 'invalid-format-index'. + * Issue broad-except and bare-except even if the number + of except handlers is different than 1. Fixes issue #113. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 7e0f3fc..c91c95d 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -239,14 +239,14 @@ class ExceptionsChecker(BaseChecker): nb_handlers = len(node.handlers) 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: + if is_empty(handler.body) and not node.orelse: 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): + if not is_raising(handler.body): self.add_message('bare-except', node=handler) # check if a "except:" is followed by some other # except - elif index < (nb_handlers - 1): + if index < (nb_handlers - 1): msg = 'empty except clause should always appear last' self.add_message('bad-except-order', node=node, args=msg) @@ -270,7 +270,7 @@ class ExceptionsChecker(BaseChecker): 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)): + and not is_raising(handler.body)): self.add_message('broad-except', args=exc.name, node=handler.type) if (not inherit_from_std_ex(exc) and diff --git a/test/input/func_block_disable_msg.py b/test/input/func_block_disable_msg.py index 2551823..480eac0 100644 --- a/test/input/func_block_disable_msg.py +++ b/test/input/func_block_disable_msg.py @@ -1,4 +1,4 @@ -# pylint: disable=C0302 +# pylint: disable=C0302,bare-except """pylint option block-disable""" __revision__ = None diff --git a/test/input/func_format_py27.py b/test/input/func_format_py27.py index 46d5cfe..eba178d 100644 --- a/test/input/func_format_py27.py +++ b/test/input/func_format_py27.py @@ -1,4 +1,4 @@ -# pylint:disable=C0103,W0104,W0105 +# pylint:disable=C0103,W0104,W0105,pointless-except """Check format """ __revision__ = '' diff --git a/test/input/func_format_py_27.py b/test/input/func_format_py_27.py index 95f7fde..be5033c 100644 --- a/test/input/func_format_py_27.py +++ b/test/input/func_format_py_27.py @@ -1,4 +1,4 @@ -# pylint:disable=C0103,W0104,W0105 +# pylint:disable=C0103,W0104,W0105,pointless-except """Check format """ __revision__ = '' diff --git a/test/messages/func_w0702.txt b/test/messages/func_w0702.txt index 7e0ea84..d40a837 100644 --- a/test/messages/func_w0702.txt +++ b/test/messages/func_w0702.txt @@ -1 +1,2 @@ W: 9: No exception type(s) specified +W: 16: Catching too general exception Exception diff --git a/test/messages/func_w0705.txt b/test/messages/func_w0705.txt index fbc200f..0393a88 100644 --- a/test/messages/func_w0705.txt +++ b/test/messages/func_w0705.txt @@ -3,3 +3,13 @@ E: 17: Bad except clauses order (LookupError is an ancestor class of IndexError) E: 24: Bad except clauses order (LookupError is an ancestor class of IndexError) E: 24: Bad except clauses order (NameError is an ancestor class of UnboundLocalError) E: 27: Bad except clauses order (empty except clause should always appear last) +W: 8: Catching too general exception Exception +W: 29: No exception type(s) specified +W: 30: Except doesn't do anything +W: 31: Catching too general exception Exception +W: 31: Except doesn't do anything +W: 38: No exception type(s) specified +W: 43: Catching too general exception Exception +W: 43: Except doesn't do anything +W: 45: No exception type(s) specified +W: 46: Except doesn't do anything \ No newline at end of file -- cgit v1.2.1 From b744a9cefde2ad685ae61816527021de33b6316c Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 27 Jun 2014 19:21:25 +0300 Subject: Fix test failures on Python 2. --- test/input/func_w0623_py_30.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/input/func_w0623_py_30.py b/test/input/func_w0623_py_30.py index 9bccbc6..1c5bd2b 100644 --- a/test/input/func_w0623_py_30.py +++ b/test/input/func_w0623_py_30.py @@ -1,5 +1,5 @@ """Test for W0623, overwriting names in exception handlers.""" - +# pylint: disable=broad-except,bare-except __revision__ = '' import exceptions -- cgit v1.2.1 From 0951ad6eed20683d5de06afb9c2dff5083ae5b2f Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 27 Jun 2014 19:23:23 +0300 Subject: Disable another warning. --- test/input/func_w0623_py_30.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/input/func_w0623_py_30.py b/test/input/func_w0623_py_30.py index 1c5bd2b..8f1f34c 100644 --- a/test/input/func_w0623_py_30.py +++ b/test/input/func_w0623_py_30.py @@ -1,5 +1,5 @@ """Test for W0623, overwriting names in exception handlers.""" -# pylint: disable=broad-except,bare-except +# pylint: disable=broad-except,bare-except,pointless-except __revision__ = '' import exceptions -- cgit v1.2.1 From 7f71cbb5cc39d981a544c6a1af50521a132ba44d Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 1 Jul 2014 13:35:52 +0300 Subject: Emit attribute-defined-outside-init for all cases, not just for the last assignment. Closes issue #262. --- ChangeLog | 3 ++ checkers/classes.py | 44 +++++++++++----------- test/input/func_defining-attr-methods_order.py | 8 ++++ test/messages/func_defining-attr-methods_order.txt | 2 + 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0c6a85c..6e3bce0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -15,6 +15,9 @@ ChangeLog for Pylint * Issue broad-except and bare-except even if the number of except handlers is different than 1. Fixes issue #113. + * Issue attribute-defined-outside-init for all cases, not just + for the last assignment. Closes issue #262. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/classes.py b/checkers/classes.py index f5e2783..02d304b 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -272,28 +272,30 @@ a metaclass class method.'} isinstance(n.statement(), (astroid.Delete, astroid.AugAssign))] if not nodes: continue # error detected by typechecking - attr_defined = False # check if any method attr is defined in is a defining method - for node in nodes: - if node.frame().name in defining_methods: - attr_defined = True - if not attr_defined: - # check attribute is defined in a parent's __init__ - for parent in cnode.instance_attr_ancestors(attr): - attr_defined = False - # check if any parent method attr is defined in is a defining method - for node in parent.instance_attrs[attr]: - if node.frame().name in defining_methods: - attr_defined = True - if attr_defined: - # we're done :) - break - else: - # check attribute is defined as a class attribute - try: - cnode.local_attr(attr) - except astroid.NotFoundError: - self.add_message('attribute-defined-outside-init', args=attr, node=node) + if any(node.frame().name in defining_methods + for node in nodes): + continue + + # check attribute is defined in a parent's __init__ + for parent in cnode.instance_attr_ancestors(attr): + attr_defined = False + # check if any parent method attr is defined in is a defining method + for node in parent.instance_attrs[attr]: + if node.frame().name in defining_methods: + attr_defined = True + if attr_defined: + # we're done :) + break + else: + # check attribute is defined as a class attribute + try: + cnode.local_attr(attr) + except astroid.NotFoundError: + for node in nodes: + if node.frame().name not in defining_methods: + self.add_message('attribute-defined-outside-init', + args=attr, node=node) def visit_function(self, node): """check method arguments, overriding""" diff --git a/test/input/func_defining-attr-methods_order.py b/test/input/func_defining-attr-methods_order.py index d64217d..888b192 100644 --- a/test/input/func_defining-attr-methods_order.py +++ b/test/input/func_defining-attr-methods_order.py @@ -26,8 +26,16 @@ class A(object): def set_z(self, z): ''' set_z docstring filler ''' self.z = z + self.z = z def setUp(self): ''' setUp docstring filler ''' self.x = 0 self.y = 0 + +class B(A): + ''' class B ''' + + def test(self): + """ test """ + self.z = 44 diff --git a/test/messages/func_defining-attr-methods_order.txt b/test/messages/func_defining-attr-methods_order.txt index 3594754..5588319 100644 --- a/test/messages/func_defining-attr-methods_order.txt +++ b/test/messages/func_defining-attr-methods_order.txt @@ -1 +1,3 @@ W: 28:A.set_z: Attribute 'z' defined outside __init__ +W: 29:A.set_z: Attribute 'z' defined outside __init__ +W: 41:B.test: Attribute 'z' defined outside __init__ -- cgit v1.2.1 From a71c0b48285566da852d5db35cf5698bcad48119 Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 1 Jul 2014 13:40:41 +0300 Subject: Remove trailing whitespace. --- checkers/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/classes.py b/checkers/classes.py index 02d304b..e550734 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -276,7 +276,7 @@ a metaclass class method.'} if any(node.frame().name in defining_methods for node in nodes): continue - + # check attribute is defined in a parent's __init__ for parent in cnode.instance_attr_ancestors(attr): attr_defined = False -- cgit v1.2.1 -- cgit v1.2.1 From 12bbddf5f91b219f9153e7ed583f91babd4b6100 Mon Sep 17 00:00:00 2001 From: David Shea Date: Tue, 1 Jul 2014 14:19:12 -0400 Subject: Emit an error when a list index is a non-integer type --- checkers/typecheck.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 25f7612..97e20e0 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -74,6 +74,9 @@ MSGS = { ('Used when a function call does not pass a mandatory' ' keyword-only argument.'), {'minversion': (3, 0)}), + 'E1126': ('List index is non-integer type %s', + 'non-integer-list-index', + 'Used when a list is indexed with a non-integer type'), } def _determine_callable(callable_obj): @@ -445,6 +448,26 @@ accessed. Python regular expressions are accepted.'} if defval is None and not assigned: self.add_message('missing-kwoa', node=node, args=(name, callable_name)) + @check_messages('non-integer-list-index') + def visit_index(self, node): + if not node.parent or not node.parent.value: + return + + # Look for index operations where the parent is a list. + # If the types can be determined, only allow indices to be ints or + # int constants. + + index_type = safe_infer(node) + parent_type = safe_infer(node.parent.value) + + if type(parent_type) == astroid.List and \ + not(index_type == astroid.YES or \ + (type(index_type) == astroid.Const and \ + type(index_type.value) == int) or \ + (type(index_type) == astroid.Instance and \ + index_type._proxied.name == 'int')): + self.add_message('non-integer-list-index', node=node, + args=(index_type,)) def register(linter): """required method to auto register this checker """ -- cgit v1.2.1 From 82255ff411d37b1a47fe98029e7f4e9d6efa85a8 Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 2 Jul 2014 16:07:05 +0300 Subject: Emit 'not-callable' when calling properties. Closes issue #268. --- ChangeLog | 2 + checkers/typecheck.py | 75 +++++++++++++++++++++- test/input/func_typecheck_non_callable_call.py | 36 +++++++++++ test/messages/func_typecheck_non_callable_call.txt | 2 + 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6e3bce0..fdc27e1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,6 +18,8 @@ ChangeLog for Pylint * Issue attribute-defined-outside-init for all cases, not just for the last assignment. Closes issue #262. + * Emit 'not-callable' when calling properties. Closes issue #268. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 25f7612..c017805 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -21,6 +21,7 @@ import shlex import astroid from astroid import InferenceError, NotFoundError, YES, Instance +from astroid.bases import BUILTINS from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker @@ -293,7 +294,72 @@ accessed. Python regular expressions are accepted.'} else: self.add_message('assignment-from-none', node=node) - @check_messages(*(MSGS.keys())) + def _check_uninferable_callfunc(self, node): + """ + Check that the given uninferable CallFunc node does not + call an actual function. + """ + if not isinstance(node.func, astroid.Getattr): + return + + # Look for properties. First, obtain + # the lhs of the Getattr node and search the attribute + # there. If that attribute is a property or a subclass of properties, + # then most likely it's not callable. + + # TODO: since astroid doesn't understand descriptors very well + # we will not handle them here, right now. + + expr = node.func.expr + klass = safe_infer(expr) + if (klass is None or klass is astroid.YES or + not isinstance(klass, astroid.Instance)): + return + + try: + attrs = klass._proxied.getattr(node.func.attrname) + except astroid.NotFoundError: + return + + stop_checking = False + for attr in attrs: + if attr is astroid.YES: + continue + if stop_checking: + break + if not isinstance(attr, astroid.Function): + continue + + # Decorated, see if it is decorated with a property + if not attr.decorators: + continue + for decorator in attr.decorators.nodes: + if not isinstance(decorator, astroid.Name): + continue + try: + for infered in decorator.infer(): + property_like = False + if isinstance(infered, astroid.Class): + if (infered.root().name == BUILTINS and + infered.name == 'property'): + property_like = True + else: + for ancestor in infered.ancestors(): + if (ancestor.name == 'property' and + ancestor.root().name == BUILTINS): + property_like = True + break + if property_like: + self.add_message('not-callable', node=node, + args=node.func.as_string()) + stop_checking = True + break + except InferenceError: + pass + if stop_checking: + break + + @check_messages(*(list(MSGS.keys()))) def visit_callfunc(self, node): """check that called functions/methods are inferred to callable objects, and that the arguments passed to the function match the parameters in @@ -315,12 +381,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('not-callable', node=node, args=node.func.as_string()) + self.add_message('not-callable', node=node, + args=node.func.as_string()) + + self._check_uninferable_callfunc(node) try: called, implicit_args, callable_name = _determine_callable(called) except ValueError: - # Any error occurred during determining the function type, most of + # Any error occurred during determining the function type, most of # those errors are handled by different warnings. return num_positional_args += implicit_args diff --git a/test/input/func_typecheck_non_callable_call.py b/test/input/func_typecheck_non_callable_call.py index 8d8a6c2..dc3e783 100644 --- a/test/input/func_typecheck_non_callable_call.py +++ b/test/input/func_typecheck_non_callable_call.py @@ -35,3 +35,39 @@ TUPLE = () INCORRECT = TUPLE() INT = 1 INCORRECT = INT() + +# Test calling properties. Pylint can detect when using only the +# getter, but it doesn't infer properly when having a getter +# and a setter. +class MyProperty(property): + """ test subclasses """ + +class PropertyTest: + """ class """ + + def __init__(self): + self.attr = 4 + + @property + def test(self): + """ Get the attribute """ + return self.attr + + @test.setter + def test(self, value): + """ Set the attribute """ + self.attr = value + + @MyProperty + def custom(self): + """ Get the attribute """ + return self.attr + + @custom.setter + def custom(self, value): + """ Set the attribute """ + self.attr = value + +PROP = PropertyTest() +PROP.test(40) +PROP.custom() diff --git a/test/messages/func_typecheck_non_callable_call.txt b/test/messages/func_typecheck_non_callable_call.txt index 0218074..8baa237 100644 --- a/test/messages/func_typecheck_non_callable_call.txt +++ b/test/messages/func_typecheck_non_callable_call.txt @@ -4,3 +4,5 @@ E: 31: LIST is not callable E: 33: DICT is not callable E: 35: TUPLE is not callable E: 37: INT is not callable +E: 72: PROP.test is not callable +E: 73: PROP.custom is not callable \ No newline at end of file -- cgit v1.2.1 From 59accca5013b2fcb4c03eecbd7d5e4a7f3892c1c Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 2 Jul 2014 16:17:05 +0300 Subject: Fix test issues. --- test/input/func_typecheck_non_callable_call.py | 44 +++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/input/func_typecheck_non_callable_call.py b/test/input/func_typecheck_non_callable_call.py index dc3e783..6d56b90 100644 --- a/test/input/func_typecheck_non_callable_call.py +++ b/test/input/func_typecheck_non_callable_call.py @@ -40,33 +40,33 @@ INCORRECT = INT() # getter, but it doesn't infer properly when having a getter # and a setter. class MyProperty(property): - """ test subclasses """ + """ test subclasses """ -class PropertyTest: - """ class """ - - def __init__(self): - self.attr = 4 +class PropertyTest(object): + """ class """ - @property - def test(self): - """ Get the attribute """ - return self.attr + def __init__(self): + self.attr = 4 - @test.setter - def test(self, value): - """ Set the attribute """ - self.attr = value + @property + def test(self): + """ Get the attribute """ + return self.attr - @MyProperty - def custom(self): - """ Get the attribute """ - return self.attr + @test.setter + def test(self, value): + """ Set the attribute """ + self.attr = value - @custom.setter - def custom(self, value): - """ Set the attribute """ - self.attr = value + @MyProperty + def custom(self): + """ Get the attribute """ + return self.attr + + @custom.setter + def custom(self, value): + """ Set the attribute """ + self.attr = value PROP = PropertyTest() PROP.test(40) -- cgit v1.2.1 From d9407a80fb8fd23b96596b38fab71d73db035ba3 Mon Sep 17 00:00:00 2001 From: David Shea Date: Thu, 3 Jul 2014 14:29:38 -0400 Subject: Add tests for list index type checking and clean up the conditional --- checkers/typecheck.py | 31 ++++++++++++++++++--------- test/input/func_non_integer_list_index.py | 29 +++++++++++++++++++++++++ test/messages/func_non_integer_list_index.txt | 4 ++++ 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 test/input/func_non_integer_list_index.py create mode 100644 test/messages/func_non_integer_list_index.txt diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 97e20e0..c9815e6 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -450,24 +450,35 @@ accessed. Python regular expressions are accepted.'} @check_messages('non-integer-list-index') def visit_index(self, node): - if not node.parent or not node.parent.value: + if not node.value or not node.parent or not node.parent.value: return # Look for index operations where the parent is a list. # If the types can be determined, only allow indices to be ints or # int constants. - index_type = safe_infer(node) parent_type = safe_infer(node.parent.value) + + if not isinstance(parent_type, astroid.List): + return + + index_type = safe_infer(node) + + if index_type is astroid.YES: + return + + # Constants must be of type int + if isinstance(index_type, astroid.Const): + if isinstance(index_type.value, int): + return + # Instance values must be of type int + elif isinstance(index_type, astroid.Instance): + if index_type.name == 'int': + return - if type(parent_type) == astroid.List and \ - not(index_type == astroid.YES or \ - (type(index_type) == astroid.Const and \ - type(index_type.value) == int) or \ - (type(index_type) == astroid.Instance and \ - index_type._proxied.name == 'int')): - self.add_message('non-integer-list-index', node=node, - args=(index_type,)) + # Anything else is an error + self.add_message('non-integer-list-index', node=node, + args=(index_type,)) def register(linter): """required method to auto register this checker """ diff --git a/test/input/func_non_integer_list_index.py b/test/input/func_non_integer_list_index.py new file mode 100644 index 0000000..eb0ef0f --- /dev/null +++ b/test/input/func_non_integer_list_index.py @@ -0,0 +1,29 @@ +"""Errors for non-integer list indicies""" + +__revision__ = 0 + +TESTLIST = [1, 2, 3] + +def function1(): + """index is a function""" + return TESTLIST[id] + +def function2(): + """index is a str constant""" + return TESTLIST['0'] + +def function3(): + """index is None""" + return TESTLIST[None] + +def function4(): + """index is a float expression""" + return TESTLIST[float(0)] + +def function5(): + """index is an int constant""" + return TESTLIST[0] # no error + +def function6(): + """index is a integer expression""" + return TESTLIST[int(0.0)] # no error diff --git a/test/messages/func_non_integer_list_index.txt b/test/messages/func_non_integer_list_index.txt new file mode 100644 index 0000000..289dda3 --- /dev/null +++ b/test/messages/func_non_integer_list_index.txt @@ -0,0 +1,4 @@ +E: 9:function1: List index is non-integer type Function(id) +E: 13:function2: List index is non-integer type Const(str) +E: 17:function3: List index is non-integer type Const(NoneType) +E: 21:function4: List index is non-integer type Instance of __builtin__.float -- cgit v1.2.1 From 0c91ba698bdfc811c96bf6a164f774cd10b9f4c2 Mon Sep 17 00:00:00 2001 From: David Shea Date: Thu, 3 Jul 2014 14:07:17 -0400 Subject: Add a check for invalid types used as slice indices. --- checkers/typecheck.py | 39 +++++++++++++++++++++++++++++ test/input/func_invalid_slice_index.py | 40 ++++++++++++++++++++++++++++++ test/messages/func_invalid_slice_index.txt | 5 ++++ 3 files changed, 84 insertions(+) create mode 100644 test/input/func_invalid_slice_index.py create mode 100644 test/messages/func_invalid_slice_index.txt diff --git a/checkers/typecheck.py b/checkers/typecheck.py index c9815e6..7283cb3 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -18,6 +18,7 @@ import re import shlex +import types import astroid from astroid import InferenceError, NotFoundError, YES, Instance @@ -77,6 +78,10 @@ MSGS = { 'E1126': ('List index is non-integer type %s', 'non-integer-list-index', 'Used when a list is indexed with a non-integer type'), + 'E1127': ('Slice index is invalid type %s', + 'invalid-slice-index', + 'Used when a slice index is not an integer, None, or an object \ + with an __index__ method.'), } def _determine_callable(callable_obj): @@ -480,6 +485,40 @@ accessed. Python regular expressions are accepted.'} self.add_message('non-integer-list-index', node=node, args=(index_type,)) + @check_messages('invalid-slice-index') + def visit_slice(self, node): + if not isinstance(node, astroid.Slice): + return + + # Check the type of each part of the slice + for index in (node.lower, node.upper, node.step): + if index is None: + continue + + index_type = safe_infer(index) + + if index_type is astroid.YES: + continue + + # Constants must of type int or None + if isinstance(index_type, astroid.Const): + if isinstance(index_type.value, (int, types.NoneType)): + continue + # Instance values must be of type int, None or an object + # with __index__ + elif isinstance(index_type, astroid.Instance): + if index_type.name in ('int', 'None'): + continue + + if any(method for method in index_type.body \ + if isinstance(method, astroid.Function) and \ + method.name == '__index__'): + continue + + # Anything else is an error + self.add_message('invalid-slice-index', node=node, + args=(index_type,)) + def register(linter): """required method to auto register this checker """ linter.register_checker(TypeChecker(linter)) diff --git a/test/input/func_invalid_slice_index.py b/test/input/func_invalid_slice_index.py new file mode 100644 index 0000000..6a3bc88 --- /dev/null +++ b/test/input/func_invalid_slice_index.py @@ -0,0 +1,40 @@ +"""Errors for invalid slice indices""" + +__revision__ = 0 + +TESTLIST = [1, 2, 3] + +def function1(): + """functions used as indices""" + return TESTLIST[id:id:] + +def function2(): + """strings used as indices""" + return TESTLIST['0':'1':] + +def function3(): + """class without __index__ used as index""" + + class NoIndexTest(object): # pylint: disable=too-few-public-methods + """Class with no __index__ method""" + pass + + return TESTLIST[NoIndexTest()::] + +def function4(): + """integers used as indices""" + return TESTLIST[0:0:0] # no error + +def function5(): + """None used as indicies""" + return TESTLIST[None:None:None] + +def function6(): + """class with __index__ used as index""" + class IndexTest(object): # pylint: disable=too-few-public-methods + """Class with __index__ method""" + def __index__(self): # pylint: disable=no-self-use + """Allow objects of this class to be used as slice indices""" + return 0 + + return TESTLIST[IndexTest():None:None] # no error diff --git a/test/messages/func_invalid_slice_index.txt b/test/messages/func_invalid_slice_index.txt new file mode 100644 index 0000000..4298e2c --- /dev/null +++ b/test/messages/func_invalid_slice_index.txt @@ -0,0 +1,5 @@ +E: 9:function1: Slice index is invalid type Function(id) +E: 9:function1: Slice index is invalid type Function(id) +E: 13:function2: Slice index is invalid type Const(str) +E: 13:function2: Slice index is invalid type Const(str) +E: 22:function3: Slice index is invalid type Instance of input.func_invalid_slice_index.NoIndexTest -- cgit v1.2.1 From 424f60d61ee19fb29e1a9244a683f6869b528e05 Mon Sep 17 00:00:00 2001 From: David Shea Date: Thu, 3 Jul 2014 14:32:20 -0400 Subject: Add a changelog entry for list and slice type checks --- ChangeLog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog b/ChangeLog index 6e3bce0..80e8853 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,6 +18,8 @@ ChangeLog for Pylint * Issue attribute-defined-outside-init for all cases, not just for the last assignment. Closes issue #262. + * Add checks for invalid list and slice indices + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. -- cgit v1.2.1 From 7f83dca69a0c5fd3b84e90d7a2a2c34a8cf086dc Mon Sep 17 00:00:00 2001 From: David Shea Date: Sun, 6 Jul 2014 13:22:14 -0400 Subject: Expand and correct the types used in the index and slice checks. Check for invalid index types used with all builtin sequence types. Allow slice objects and instances with __index__ to be used as indices. Check for __index__ implemented in bases of an instance. Remove the astroid types from the error messages and correct the wording. Use .pytype() instead of .name to check types to avoid problems with custom types defining names that conflict with the bulitin types. Use type(None) instead of types.NoneType to avoid python 3 incompatibilities. Handle None returned by safe_infer. --- ChangeLog | 2 +- checkers/typecheck.py | 62 +++++++++++++------- test/input/func_invalid_sequence_index.py | 84 +++++++++++++++++++++++++++ test/input/func_invalid_slice_index.py | 18 ++++++ test/input/func_non_integer_list_index.py | 29 --------- test/messages/func_invalid_sequence_index.txt | 7 +++ test/messages/func_invalid_slice_index.txt | 10 ++-- test/messages/func_non_integer_list_index.txt | 4 -- 8 files changed, 155 insertions(+), 61 deletions(-) create mode 100644 test/input/func_invalid_sequence_index.py delete mode 100644 test/input/func_non_integer_list_index.py create mode 100644 test/messages/func_invalid_sequence_index.txt delete mode 100644 test/messages/func_non_integer_list_index.txt diff --git a/ChangeLog b/ChangeLog index 80e8853..011e458 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,7 +18,7 @@ ChangeLog for Pylint * Issue attribute-defined-outside-init for all cases, not just for the last assignment. Closes issue #262. - * Add checks for invalid list and slice indices + * Add checks for invalid sequence and slice indices 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 7283cb3..dc64ead 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -19,6 +19,7 @@ import re import shlex import types +import sys import astroid from astroid import InferenceError, NotFoundError, YES, Instance @@ -75,15 +76,20 @@ MSGS = { ('Used when a function call does not pass a mandatory' ' keyword-only argument.'), {'minversion': (3, 0)}), - 'E1126': ('List index is non-integer type %s', - 'non-integer-list-index', + 'E1126': ('Sequence index is not an int, slice, or instance with __index__', + 'invalid-sequence-index', 'Used when a list is indexed with a non-integer type'), - 'E1127': ('Slice index is invalid type %s', + 'E1127': ('Slice index is not an int, None, or instance with __index__', 'invalid-slice-index', 'Used when a slice index is not an integer, None, or an object \ with an __index__ method.'), } +if sys.version_info >= (3,0): + BUILTINS = 'builtins' +else: + BUILTINS = '__builtin__' + def _determine_callable(callable_obj): # Ordering is important, since BoundMethod is a subclass of UnboundMethod, # and Function inherits Lambda. @@ -453,37 +459,47 @@ accessed. Python regular expressions are accepted.'} if defval is None and not assigned: self.add_message('missing-kwoa', node=node, args=(name, callable_name)) - @check_messages('non-integer-list-index') + @check_messages('invalid-sequence-index') def visit_index(self, node): if not node.value or not node.parent or not node.parent.value: return - # Look for index operations where the parent is a list. - # If the types can be determined, only allow indices to be ints or - # int constants. + # Look for index operations where the parent is a sequence type. + # If the types can be determined, only allow indices to be int, + # slice or instances with __index__. + + sequence_types = set('%s.%s' % (BUILTINS, s) for s in + ('str', 'unicode', 'list', 'tuple', 'bytearray', + 'xrange', 'range', 'bytes', 'memoryview')) parent_type = safe_infer(node.parent.value) - if not isinstance(parent_type, astroid.List): + if not parent_type or parent_type.pytype() not in sequence_types: return index_type = safe_infer(node) - if index_type is astroid.YES: + if index_type is None or index_type is astroid.YES: return # Constants must be of type int if isinstance(index_type, astroid.Const): if isinstance(index_type.value, int): return - # Instance values must be of type int + # Instance values must be int, slice, or have an __index__ method elif isinstance(index_type, astroid.Instance): - if index_type.name == 'int': + if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'): + return + + try: + index_type.getattr('__index__') + except astroid.NotFoundError: + pass + else: return # Anything else is an error - self.add_message('non-integer-list-index', node=node, - args=(index_type,)) + self.add_message('invalid-sequence-index', node=node) @check_messages('invalid-slice-index') def visit_slice(self, node): @@ -497,27 +513,29 @@ accessed. Python regular expressions are accepted.'} index_type = safe_infer(index) - if index_type is astroid.YES: + if index_type is None or index_type is astroid.YES: continue # Constants must of type int or None if isinstance(index_type, astroid.Const): - if isinstance(index_type.value, (int, types.NoneType)): + if isinstance(index_type.value, (int, type(None))): continue # Instance values must be of type int, None or an object # with __index__ elif isinstance(index_type, astroid.Instance): - if index_type.name in ('int', 'None'): + if index_type.pytype() in (BUILTINS + '.int', + BUILTINS + '.NoneType'): continue - if any(method for method in index_type.body \ - if isinstance(method, astroid.Function) and \ - method.name == '__index__'): - continue + try: + index_type.getattr('__index__') + except astroid.NotFoundError: + pass + else: + return # Anything else is an error - self.add_message('invalid-slice-index', node=node, - args=(index_type,)) + self.add_message('invalid-slice-index', node=node) def register(linter): """required method to auto register this checker """ diff --git a/test/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py new file mode 100644 index 0000000..f00110e --- /dev/null +++ b/test/input/func_invalid_sequence_index.py @@ -0,0 +1,84 @@ +"""Errors for invalid sequence indices""" + +__revision__ = 0 + +TESTLIST = [1, 2, 3] +TESTTUPLE = (1, 2, 3) +TESTSTR = '123' + +def function1(): + """list index is a function""" + return TESTLIST[id] + +def function2(): + """list index is a str constant""" + return TESTLIST['0'] + +def function3(): + """list index is None""" + return TESTLIST[None] + +def function4(): + """list index is a float expression""" + return TESTLIST[float(0)] + +def function5(): + """list index is an int constant""" + return TESTLIST[0] # no error + +def function6(): + """list index is a integer expression""" + return TESTLIST[int(0.0)] # no error + +def function7(): + """list index is a slice""" + return TESTLIST[slice(1, 2, 3)] # no error + +def function8(): + """list index implements __index__""" + class IndexType(object): # pylint: disable=too-few-public-methods + """Class with __index__ method""" + def __index__(self): # pylint: disable=no-self-use + """Allow objects of this class to be used as slice indices""" + return 0 + + return TESTLIST[IndexType()] # no error + +def function9(): + """list index implements __index__ in a superclass""" + class IndexType(object): # pylint: disable=too-few-public-methods + """Class with __index__ method""" + def __index__(self): # pylint: disable=no-self-use + """Allow objects of this class to be used as slice indices""" + return 0 + + class IndexSubType(IndexType): # pylint: disable=too-few-public-methods + """Class with __index__ in parent""" + pass + + return TESTLIST[IndexSubType()] # no error + +def function10(): + """list index does not implement __index__""" + class NonIndexType(object): # pylint: disable=too-few-public-methods + """Class without __index__ method""" + pass + + return TESTLIST[NonIndexType()] + +# Repeat a handful of tests to ensure non-list types are caught +def function11(): + """Tuple index is None""" + return TESTTUPLE[None] + +def function12(): + """Tuple index is an int constant""" + return TESTTUPLE[0] + +def function13(): + """String index is None""" + return TESTSTR[None] + +def function14(): + """String index is an int constant""" + return TESTSTR[0] diff --git a/test/input/func_invalid_slice_index.py b/test/input/func_invalid_slice_index.py index 6a3bc88..b634c90 100644 --- a/test/input/func_invalid_slice_index.py +++ b/test/input/func_invalid_slice_index.py @@ -38,3 +38,21 @@ def function6(): return 0 return TESTLIST[IndexTest():None:None] # no error + +def function7(): + """class with __index__ in superclass used as index""" + class IndexType(object): # pylint: disable=too-few-public-methods + """Class with __index__ method""" + def __index__(self): # pylint: disable=no-self-use + """Allow objects of this class to be used as slice indices""" + return 0 + + class IndexSubType(IndexType): # pylint: disable=too-few-public-methods + """Class with __index__ in parent""" + pass + + return TESTLIST[IndexSubType():None:None] # no error + +def function8(): + """slice object used as index""" + return TESTLIST[slice(1, 2, 3)] diff --git a/test/input/func_non_integer_list_index.py b/test/input/func_non_integer_list_index.py deleted file mode 100644 index eb0ef0f..0000000 --- a/test/input/func_non_integer_list_index.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Errors for non-integer list indicies""" - -__revision__ = 0 - -TESTLIST = [1, 2, 3] - -def function1(): - """index is a function""" - return TESTLIST[id] - -def function2(): - """index is a str constant""" - return TESTLIST['0'] - -def function3(): - """index is None""" - return TESTLIST[None] - -def function4(): - """index is a float expression""" - return TESTLIST[float(0)] - -def function5(): - """index is an int constant""" - return TESTLIST[0] # no error - -def function6(): - """index is a integer expression""" - return TESTLIST[int(0.0)] # no error diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt new file mode 100644 index 0000000..198c756 --- /dev/null +++ b/test/messages/func_invalid_sequence_index.txt @@ -0,0 +1,7 @@ +E: 11:function1: Sequence index is not an int, slice, or instance with __index__ +E: 15:function2: Sequence index is not an int, slice, or instance with __index__ +E: 19:function3: Sequence index is not an int, slice, or instance with __index__ +E: 23:function4: Sequence index is not an int, slice, or instance with __index__ +E: 67:function10: Sequence index is not an int, slice, or instance with __index__ +E: 72:function11: Sequence index is not an int, slice, or instance with __index__ +E: 80:function13: Sequence index is not an int, slice, or instance with __index__ diff --git a/test/messages/func_invalid_slice_index.txt b/test/messages/func_invalid_slice_index.txt index 4298e2c..2ffcd59 100644 --- a/test/messages/func_invalid_slice_index.txt +++ b/test/messages/func_invalid_slice_index.txt @@ -1,5 +1,5 @@ -E: 9:function1: Slice index is invalid type Function(id) -E: 9:function1: Slice index is invalid type Function(id) -E: 13:function2: Slice index is invalid type Const(str) -E: 13:function2: Slice index is invalid type Const(str) -E: 22:function3: Slice index is invalid type Instance of input.func_invalid_slice_index.NoIndexTest +E: 9:function1: Slice index is not an int, None, or instance with __index__ +E: 9:function1: Slice index is not an int, None, or instance with __index__ +E: 13:function2: Slice index is not an int, None, or instance with __index__ +E: 13:function2: Slice index is not an int, None, or instance with __index__ +E: 22:function3: Slice index is not an int, None, or instance with __index__ diff --git a/test/messages/func_non_integer_list_index.txt b/test/messages/func_non_integer_list_index.txt deleted file mode 100644 index 289dda3..0000000 --- a/test/messages/func_non_integer_list_index.txt +++ /dev/null @@ -1,4 +0,0 @@ -E: 9:function1: List index is non-integer type Function(id) -E: 13:function2: List index is non-integer type Const(str) -E: 17:function3: List index is non-integer type Const(NoneType) -E: 21:function4: List index is non-integer type Instance of __builtin__.float -- cgit v1.2.1 From 10d423fce46d954b2a6267c11aa3c30644a02dd7 Mon Sep 17 00:00:00 2001 From: David Shea Date: Sun, 6 Jul 2014 13:21:14 -0400 Subject: Correct a typo: indicies -> indices --- test/input/func_invalid_slice_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/input/func_invalid_slice_index.py b/test/input/func_invalid_slice_index.py index b634c90..583fb94 100644 --- a/test/input/func_invalid_slice_index.py +++ b/test/input/func_invalid_slice_index.py @@ -26,7 +26,7 @@ def function4(): return TESTLIST[0:0:0] # no error def function5(): - """None used as indicies""" + """None used as indices""" return TESTLIST[None:None:None] def function6(): -- cgit v1.2.1 From b5434608cac4d5f3af980b71b5e79b48e96e476d Mon Sep 17 00:00:00 2001 From: David Shea Date: Sun, 6 Jul 2014 14:57:00 -0400 Subject: Check for invalid indices used with subclasses of sequence types. --- checkers/typecheck.py | 29 ++++++++++++++++++--- test/input/func_invalid_sequence_index.py | 37 +++++++++++++++++++++++++++ test/messages/func_invalid_sequence_index.txt | 1 + 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index dc64ead..cc0c173 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -468,13 +468,34 @@ accessed. Python regular expressions are accepted.'} # If the types can be determined, only allow indices to be int, # slice or instances with __index__. - sequence_types = set('%s.%s' % (BUILTINS, s) for s in - ('str', 'unicode', 'list', 'tuple', 'bytearray', - 'xrange', 'range', 'bytes', 'memoryview')) + sequence_types = set(['str', 'unicode', 'list', 'tuple', 'bytearray', + 'xrange', 'range', 'bytes', 'memoryview']) parent_type = safe_infer(node.parent.value) - if not parent_type or parent_type.pytype() not in sequence_types: + if not parent_type: + return + + # Check if this instance has a __getitem__ method implemented + # in a bulitin sequence type. This way we catch subclasses of + # sequence types but skip classes that override __getitem__ and + # which may allow non-integer indices. + try: + getitem = parent_type.getattr('__getitem__')[0] + except (astroid.NotFoundError, IndexError, TypeError): + return + + if not isinstance(getitem, astroid.Function): + return + + if not getitem.parent: + return + + if not isinstance(getitem.parent.parent, astroid.Module) or \ + getitem.parent.parent.name != BUILTINS: + return + + if not getitem.parent.name in sequence_types: return index_type = safe_infer(node) diff --git a/test/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py index f00110e..29a8a62 100644 --- a/test/input/func_invalid_sequence_index.py +++ b/test/input/func_invalid_sequence_index.py @@ -82,3 +82,40 @@ def function13(): def function14(): """String index is an int constant""" return TESTSTR[0] + +def function15(): + """Index of subclass of tuple is None""" + class TupleTest(tuple): # pylint: disable=too-few-public-methods + """Subclass of tuple""" + pass + return TupleTest()[None] + +def function16(): + """Index of subclass of tuple is an int constant""" + class TupleTest(tuple): # pylint: disable=too-few-public-methods + """Subclass of tuple""" + pass + return TupleTest()[0] # no error + +def function17(): + """Index of subclass of tuple with custom __getitem__ is None""" + class TupleTest(tuple): # pylint: disable=too-few-public-methods + """Subclass of tuple with custom __getitem__""" + def __getitem__(self, index): # pylint: disable=no-self-use + """Allow non-integer indices""" + return 0 + return TupleTest()[None] # no error + +def function18(): + """Index of subclass of tuple with __getitem__ in superclass is None""" + class TupleTest(tuple): # pylint: disable=too-few-public-methods + """Subclass of tuple with custom __getitem__""" + def __getitem__(self, index): # pylint: disable=no-self-use + """Allow non-integer indices""" + return 0 + + class SubTupleTest(TupleTest): # pylint: disable=too-few-public-methods + """Subclass of a subclass of tuple""" + pass + + return SubTupleTest()[None] # no error diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt index 198c756..d7b72fd 100644 --- a/test/messages/func_invalid_sequence_index.txt +++ b/test/messages/func_invalid_sequence_index.txt @@ -5,3 +5,4 @@ E: 23:function4: Sequence index is not an int, slice, or instance with __index__ E: 67:function10: Sequence index is not an int, slice, or instance with __index__ E: 72:function11: Sequence index is not an int, slice, or instance with __index__ E: 80:function13: Sequence index is not an int, slice, or instance with __index__ +E: 91:function15: Sequence index is not an int, slice, or instance with __index__ -- cgit v1.2.1 From 0cf699cb531782c51065e7da33ca06385216115c Mon Sep 17 00:00:00 2001 From: David Shea Date: Sun, 6 Jul 2014 15:15:50 -0400 Subject: Remove the types import since it is no longer used --- checkers/typecheck.py | 1 - 1 file changed, 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 0c52d8e..fb9ed59 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -18,7 +18,6 @@ import re import shlex -import types import sys import astroid -- cgit v1.2.1 From 9b46e20c59729b46135ce777c6b30a164927798a Mon Sep 17 00:00:00 2001 From: David Shea Date: Mon, 7 Jul 2014 10:00:29 -0400 Subject: Expand the description for invalid-sequence-index to match the check. --- checkers/typecheck.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index fb9ed59..395e1b7 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -78,7 +78,8 @@ MSGS = { {'minversion': (3, 0)}), 'E1126': ('Sequence index is not an int, slice, or instance with __index__', 'invalid-sequence-index', - 'Used when a list is indexed with a non-integer type'), + 'Used when a sequence type is indexed with an invalid type. Valid \ + types are ints, slices, and objects with an __index__ method.'), 'E1127': ('Slice index is not an int, None, or instance with __index__', 'invalid-slice-index', 'Used when a slice index is not an integer, None, or an object \ -- cgit v1.2.1 From 2a45373090e129807c45c4b7d9eb35a4ea94f117 Mon Sep 17 00:00:00 2001 From: David Shea Date: Mon, 7 Jul 2014 10:10:56 -0400 Subject: Clean up some stylistic issues in the sequence and slice index type checks. --- checkers/typecheck.py | 23 +++++++++-------------- test/input/func_invalid_sequence_index.py | 27 ++++++++++++++------------- test/input/func_invalid_slice_index.py | 13 +++++++------ test/messages/func_invalid_sequence_index.txt | 16 ++++++++-------- test/messages/func_invalid_slice_index.txt | 10 +++++----- 5 files changed, 43 insertions(+), 46 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 395e1b7..f0c8034 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -86,11 +86,6 @@ MSGS = { with an __index__ method.'), } -if sys.version_info >= (3,0): - BUILTINS = 'builtins' -else: - BUILTINS = '__builtin__' - def _determine_callable(callable_obj): # Ordering is important, since BoundMethod is a subclass of UnboundMethod, # and Function inherits Lambda. @@ -550,8 +545,11 @@ accessed. Python regular expressions are accepted.'} # sequence types but skip classes that override __getitem__ and # which may allow non-integer indices. try: - getitem = parent_type.getattr('__getitem__')[0] - except (astroid.NotFoundError, IndexError, TypeError): + getitems = parent_type.getattr('__getitem__') + if getitems is astroid.YES: + return + getitem = getitems[0] + except (astroid.NotFoundError, IndexError): return if not isinstance(getitem, astroid.Function): @@ -560,11 +558,10 @@ accessed. Python regular expressions are accepted.'} if not getitem.parent: return - if not isinstance(getitem.parent.parent, astroid.Module) or \ - getitem.parent.parent.name != BUILTINS: + if getitem.root() != BUILTINS: return - if not getitem.parent.name in sequence_types: + if getitem.parent.name not in sequence_types: return index_type = safe_infer(node) @@ -583,10 +580,9 @@ accessed. Python regular expressions are accepted.'} try: index_type.getattr('__index__') + return except astroid.NotFoundError: pass - else: - return # Anything else is an error self.add_message('invalid-sequence-index', node=node) @@ -619,10 +615,9 @@ accessed. Python regular expressions are accepted.'} try: index_type.getattr('__index__') + return except astroid.NotFoundError: pass - else: - return # Anything else is an error self.add_message('invalid-slice-index', node=node) diff --git a/test/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py index 29a8a62..c1d6e72 100644 --- a/test/input/func_invalid_sequence_index.py +++ b/test/input/func_invalid_sequence_index.py @@ -1,4 +1,5 @@ """Errors for invalid sequence indices""" +# pylint: disable=too-few-public-methods, no-self-use __revision__ = 0 @@ -36,9 +37,9 @@ def function7(): def function8(): """list index implements __index__""" - class IndexType(object): # pylint: disable=too-few-public-methods + class IndexType(object): """Class with __index__ method""" - def __index__(self): # pylint: disable=no-self-use + def __index__(self): """Allow objects of this class to be used as slice indices""" return 0 @@ -46,13 +47,13 @@ def function8(): def function9(): """list index implements __index__ in a superclass""" - class IndexType(object): # pylint: disable=too-few-public-methods + class IndexType(object): """Class with __index__ method""" - def __index__(self): # pylint: disable=no-self-use + def __index__(self): """Allow objects of this class to be used as slice indices""" return 0 - class IndexSubType(IndexType): # pylint: disable=too-few-public-methods + class IndexSubType(IndexType): """Class with __index__ in parent""" pass @@ -60,7 +61,7 @@ def function9(): def function10(): """list index does not implement __index__""" - class NonIndexType(object): # pylint: disable=too-few-public-methods + class NonIndexType(object): """Class without __index__ method""" pass @@ -85,36 +86,36 @@ def function14(): def function15(): """Index of subclass of tuple is None""" - class TupleTest(tuple): # pylint: disable=too-few-public-methods + class TupleTest(tuple): """Subclass of tuple""" pass return TupleTest()[None] def function16(): """Index of subclass of tuple is an int constant""" - class TupleTest(tuple): # pylint: disable=too-few-public-methods + class TupleTest(tuple): """Subclass of tuple""" pass return TupleTest()[0] # no error def function17(): """Index of subclass of tuple with custom __getitem__ is None""" - class TupleTest(tuple): # pylint: disable=too-few-public-methods + class TupleTest(tuple): """Subclass of tuple with custom __getitem__""" - def __getitem__(self, index): # pylint: disable=no-self-use + def __getitem__(self, index): """Allow non-integer indices""" return 0 return TupleTest()[None] # no error def function18(): """Index of subclass of tuple with __getitem__ in superclass is None""" - class TupleTest(tuple): # pylint: disable=too-few-public-methods + class TupleTest(tuple): """Subclass of tuple with custom __getitem__""" - def __getitem__(self, index): # pylint: disable=no-self-use + def __getitem__(self, index): """Allow non-integer indices""" return 0 - class SubTupleTest(TupleTest): # pylint: disable=too-few-public-methods + class SubTupleTest(TupleTest): """Subclass of a subclass of tuple""" pass diff --git a/test/input/func_invalid_slice_index.py b/test/input/func_invalid_slice_index.py index 583fb94..fa72b8c 100644 --- a/test/input/func_invalid_slice_index.py +++ b/test/input/func_invalid_slice_index.py @@ -1,4 +1,5 @@ """Errors for invalid slice indices""" +# pylint: disable=too-few-public-methods, no-self-use __revision__ = 0 @@ -15,7 +16,7 @@ def function2(): def function3(): """class without __index__ used as index""" - class NoIndexTest(object): # pylint: disable=too-few-public-methods + class NoIndexTest(object): """Class with no __index__ method""" pass @@ -31,9 +32,9 @@ def function5(): def function6(): """class with __index__ used as index""" - class IndexTest(object): # pylint: disable=too-few-public-methods + class IndexTest(object): """Class with __index__ method""" - def __index__(self): # pylint: disable=no-self-use + def __index__(self): """Allow objects of this class to be used as slice indices""" return 0 @@ -41,13 +42,13 @@ def function6(): def function7(): """class with __index__ in superclass used as index""" - class IndexType(object): # pylint: disable=too-few-public-methods + class IndexType(object): """Class with __index__ method""" - def __index__(self): # pylint: disable=no-self-use + def __index__(self): """Allow objects of this class to be used as slice indices""" return 0 - class IndexSubType(IndexType): # pylint: disable=too-few-public-methods + class IndexSubType(IndexType): """Class with __index__ in parent""" pass diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt index d7b72fd..79a5956 100644 --- a/test/messages/func_invalid_sequence_index.txt +++ b/test/messages/func_invalid_sequence_index.txt @@ -1,8 +1,8 @@ -E: 11:function1: Sequence index is not an int, slice, or instance with __index__ -E: 15:function2: Sequence index is not an int, slice, or instance with __index__ -E: 19:function3: Sequence index is not an int, slice, or instance with __index__ -E: 23:function4: Sequence index is not an int, slice, or instance with __index__ -E: 67:function10: Sequence index is not an int, slice, or instance with __index__ -E: 72:function11: Sequence index is not an int, slice, or instance with __index__ -E: 80:function13: Sequence index is not an int, slice, or instance with __index__ -E: 91:function15: Sequence index is not an int, slice, or instance with __index__ +E: 12:function1: Sequence index is not an int, slice, or instance with __index__ +E: 16:function2: Sequence index is not an int, slice, or instance with __index__ +E: 20:function3: Sequence index is not an int, slice, or instance with __index__ +E: 24:function4: Sequence index is not an int, slice, or instance with __index__ +E: 68:function10: Sequence index is not an int, slice, or instance with __index__ +E: 73:function11: Sequence index is not an int, slice, or instance with __index__ +E: 81:function13: Sequence index is not an int, slice, or instance with __index__ +E: 92:function15: Sequence index is not an int, slice, or instance with __index__ diff --git a/test/messages/func_invalid_slice_index.txt b/test/messages/func_invalid_slice_index.txt index 2ffcd59..d6882b1 100644 --- a/test/messages/func_invalid_slice_index.txt +++ b/test/messages/func_invalid_slice_index.txt @@ -1,5 +1,5 @@ -E: 9:function1: Slice index is not an int, None, or instance with __index__ -E: 9:function1: Slice index is not an int, None, or instance with __index__ -E: 13:function2: Slice index is not an int, None, or instance with __index__ -E: 13:function2: Slice index is not an int, None, or instance with __index__ -E: 22:function3: Slice index is not an int, None, or instance with __index__ +E: 10:function1: Slice index is not an int, None, or instance with __index__ +E: 10:function1: Slice index is not an int, None, or instance with __index__ +E: 14:function2: Slice index is not an int, None, or instance with __index__ +E: 14:function2: Slice index is not an int, None, or instance with __index__ +E: 23:function3: Slice index is not an int, None, or instance with __index__ -- cgit v1.2.1 From eef3e13af5c0e6fcfea055e7162e4e11b0ba3365 Mon Sep 17 00:00:00 2001 From: David Shea Date: Mon, 7 Jul 2014 10:45:57 -0400 Subject: Correct the use of root() --- checkers/typecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index f0c8034..d3a1aae 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -558,7 +558,7 @@ accessed. Python regular expressions are accepted.'} if not getitem.parent: return - if getitem.root() != BUILTINS: + if getitem.root().name != BUILTINS: return if getitem.parent.name not in sequence_types: -- cgit v1.2.1 From 2465aa80aa877497bc723caa253e0243188a035a Mon Sep 17 00:00:00 2001 From: David Shea Date: Mon, 7 Jul 2014 14:20:47 -0400 Subject: Correct the sequence index check for set and delete operations. Usage of an index does not always imply __getitem__. --- checkers/typecheck.py | 33 +++++++++----- test/input/func_invalid_sequence_index.py | 65 +++++++++++++++++++++++++++ test/messages/func_invalid_sequence_index.txt | 10 +++++ 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index d3a1aae..169abda 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -540,28 +540,39 @@ accessed. Python regular expressions are accepted.'} if not parent_type: return - # Check if this instance has a __getitem__ method implemented - # in a bulitin sequence type. This way we catch subclasses of - # sequence types but skip classes that override __getitem__ and - # which may allow non-integer indices. + # Determine what method on the parent this index will use + # The parent of this node will be a Subscript, and the parent of that + # node determines if the Subscript is a get, set, or delete operation. + operation = node.parent.parent + if isinstance(operation, astroid.Assign): + methodname = '__setitem__' + elif isinstance(operation, astroid.Delete): + methodname = '__delitem__' + else: + methodname = '__getitem__' + + # Check if this instance's __getitem__, __setitem__, or __delitem__, as + # appropriate to the statement, is implemented in a bulitin sequence + # type. This way we catch subclasses of sequence types but skip classes + # that override __getitem__ and which may allow non-integer indices. try: - getitems = parent_type.getattr('__getitem__') - if getitems is astroid.YES: + methods = parent_type.getattr(methodname) + if methods is astroid.YES: return - getitem = getitems[0] + itemmethod = methods[0] except (astroid.NotFoundError, IndexError): return - if not isinstance(getitem, astroid.Function): + if not isinstance(itemmethod, astroid.Function): return - if not getitem.parent: + if itemmethod.root().name != BUILTINS: return - if getitem.root().name != BUILTINS: + if not itemmethod.parent: return - if getitem.parent.name not in sequence_types: + if itemmethod.parent.name not in sequence_types: return index_type = safe_infer(node) diff --git a/test/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py index c1d6e72..d0c8304 100644 --- a/test/input/func_invalid_sequence_index.py +++ b/test/input/func_invalid_sequence_index.py @@ -120,3 +120,68 @@ def function18(): pass return SubTupleTest()[None] # no error + +# Test with set and delete statements + +def function19(): + """Set with None and integer indices""" + TESTLIST[None] = 0 + TESTLIST[0] = 0 # no error + +def function20(): + """Delete with None and integer indicies""" + del TESTLIST[None] + del TESTLIST[0] # no error + +def function21(): + """Set and delete on a subclass of list""" + class ListTest(list): + """Inherit all list get/set/del handlers""" + pass + test = ListTest() + test[None] = 0 + test[0] = 0 # no error + del test[None] + del test[0] # no error + +def function22(): + """Get, set, and delete on a subclass of list that overrides __setitem__""" + class ListTest(list): + """Override setitem but not get or del""" + def __setitem__(self, key, value): + pass + test = ListTest() + test[None][0] = 0 + test[0][0] = 0 # no error + test[None] = 0 # no error + test[0] = 0 # no error + del test[None] + del test[0] # no error + +def function23(): + """Get, set, and delete on a subclass of list that overrides __delitem__""" + class ListTest(list): + """Override delitem but not get or set""" + def __delitem__(self, key): + pass + test = ListTest() + test[None][0] = 0 + test[0][0] = 0 # no error + test[None] = 0 + test[0] = 0 # no error + del test[None] # no error + del test[0] # no error + +def function24(): + """Get, set, and delete on a subclass of list that overrides __getitem__""" + class ListTest(list): + """Override gelitem but not del or set""" + def __getitem__(self, key): + pass + test = ListTest() + test[None][0] = 0 # no error + test[0][0] = 0 # no error + test[None] = 0 + test[0] = 0 # no error + del test[None] + del test[0] # no error diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt index 79a5956..cc58938 100644 --- a/test/messages/func_invalid_sequence_index.txt +++ b/test/messages/func_invalid_sequence_index.txt @@ -6,3 +6,13 @@ E: 68:function10: Sequence index is not an int, slice, or instance with __index_ E: 73:function11: Sequence index is not an int, slice, or instance with __index__ E: 81:function13: Sequence index is not an int, slice, or instance with __index__ E: 92:function15: Sequence index is not an int, slice, or instance with __index__ +E:128:function19: Sequence index is not an int, slice, or instance with __index__ +E:133:function20: Sequence index is not an int, slice, or instance with __index__ +E:142:function21: Sequence index is not an int, slice, or instance with __index__ +E:144:function21: Sequence index is not an int, slice, or instance with __index__ +E:154:function22: Sequence index is not an int, slice, or instance with __index__ +E:158:function22: Sequence index is not an int, slice, or instance with __index__ +E:168:function23: Sequence index is not an int, slice, or instance with __index__ +E:170:function23: Sequence index is not an int, slice, or instance with __index__ +E:184:function24: Sequence index is not an int, slice, or instance with __index__ +E:186:function24: Sequence index is not an int, slice, or instance with __index__ -- cgit v1.2.1 From adb9af53e81c1708404ad0123c0da23f1c267c94 Mon Sep 17 00:00:00 2001 From: David Shea Date: Mon, 7 Jul 2014 14:44:25 -0400 Subject: Correct problems with checking extended slice syntax. Fix a crash triggered by visit_index events on ExtSlice objects, and emit an error when extslice syntax is used with builtin sequence types. --- checkers/typecheck.py | 16 ++++++++++++++-- test/input/func_invalid_sequence_index.py | 12 ++++++++++++ test/messages/func_invalid_sequence_index.txt | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 169abda..1869d5f 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -523,9 +523,15 @@ accessed. Python regular expressions are accepted.'} if defval is None and not assigned: self.add_message('missing-kwoa', node=node, args=(name, callable_name)) + @check_messages('invalid-sequence-index') + def visit_extslice(self, node): + # Check extended slice objects as if they were used as a sequence + # index to check if the object being sliced can support them + return self.visit_index(node) + @check_messages('invalid-sequence-index') def visit_index(self, node): - if not node.value or not node.parent or not node.parent.value: + if not node.parent or not hasattr(node.parent, "value"): return # Look for index operations where the parent is a sequence type. @@ -575,7 +581,13 @@ accessed. Python regular expressions are accepted.'} if itemmethod.parent.name not in sequence_types: return - index_type = safe_infer(node) + # For ExtSlice objects coming from visit_extslice, no further + # inference is necessary, since if we got this far the ExtSlice + # is an error. + if isinstance(node, astroid.ExtSlice): + index_type = node + else: + index_type = safe_infer(node) if index_type is None or index_type is astroid.YES: return diff --git a/test/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py index d0c8304..854eabe 100644 --- a/test/input/func_invalid_sequence_index.py +++ b/test/input/func_invalid_sequence_index.py @@ -185,3 +185,15 @@ def function24(): test[0] = 0 # no error del test[None] del test[0] # no error + +def function25(): + """Extended slice used with a list""" + return TESTLIST[..., 0] + +def function26(): + """Extended slice used with an object that implements __getitem__""" + class ExtSliceTest(object): + """Permit extslice syntax by implementing __getitem__""" + def __getitem__(self, index): + return 0 + return ExtSliceTest[..., 0] # no error diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt index cc58938..a9b172a 100644 --- a/test/messages/func_invalid_sequence_index.txt +++ b/test/messages/func_invalid_sequence_index.txt @@ -16,3 +16,4 @@ E:168:function23: Sequence index is not an int, slice, or instance with __index_ E:170:function23: Sequence index is not an int, slice, or instance with __index__ E:184:function24: Sequence index is not an int, slice, or instance with __index__ E:186:function24: Sequence index is not an int, slice, or instance with __index__ +E:191:function25: Sequence index is not an int, slice, or instance with __index__ -- cgit v1.2.1 From fac1865c061d62ddd5a8473a6f6d3fed9134d047 Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 8 Jul 2014 15:24:45 +0300 Subject: Fix a false positive with unbalanced iterable unpacking, when encountering starred nodes. Closes issue #273. --- ChangeLog | 3 +++ checkers/variables.py | 12 ++++++++---- test/input/func_noerror_unbalanced_tuple_unpacking_py30.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 test/input/func_noerror_unbalanced_tuple_unpacking_py30.py diff --git a/ChangeLog b/ChangeLog index fdc27e1..f56cfe2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -20,6 +20,9 @@ ChangeLog for Pylint * Emit 'not-callable' when calling properties. Closes issue #268. + * Fix a false positive with unbalanced iterable unpacking, + when encountering starred nodes. Closes issue #273. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/variables.py b/checkers/variables.py index dc8d111..b84ad4f 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -147,7 +147,7 @@ MSGS = { 'a sequence is used in an unpack assignment'), 'W0640': ('Cell variable %s defined in loop', - 'cell-var-from-loop', + 'cell-var-from-loop', 'A variable used in a closure is defined in a loop. ' 'This will result in all closures using the same value for ' 'the closed-over variable.'), @@ -447,7 +447,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' else: if maybe_for.parent_of(node_scope) and not isinstance(node_scope.statement(), astroid.Return): self.add_message('cell-var-from-loop', node=node, args=node.name) - + def _loopvar_name(self, node, name): # filter variables according to node's scope # XXX used to filter parents but don't remember why, and removing this @@ -649,6 +649,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' # attempt to check unpacking is properly balanced values = infered.itered() if len(targets) != len(values): + # Check if we have starred nodes. + if any(isinstance(target, astroid.Starred) + for target in targets): + return self.add_message('unbalanced-tuple-unpacking', node=node, args=(_get_unpacking_extra_info(node, infered), len(targets), @@ -727,8 +731,8 @@ class VariablesChecker3k(VariablesChecker): if isinstance(klass._metaclass, astroid.Name): module_locals.pop(klass._metaclass.name, None) - if metaclass: - # if it uses a `metaclass=module.Class` + if metaclass: + # if it uses a `metaclass=module.Class` module_locals.pop(metaclass.root().name, None) super(VariablesChecker3k, self).leave_module(node) diff --git a/test/input/func_noerror_unbalanced_tuple_unpacking_py30.py b/test/input/func_noerror_unbalanced_tuple_unpacking_py30.py new file mode 100644 index 0000000..68f5fb7 --- /dev/null +++ b/test/input/func_noerror_unbalanced_tuple_unpacking_py30.py @@ -0,0 +1,11 @@ +""" Test that using starred nodes in unpacking +does not trigger a false positive on Python 3. +""" + +__revision__ = 1 + +def test(): + """ Test that starred expressions don't give false positives. """ + first, second, *last = (1, 2, 3, 4) + *last, = (1, 2) + return (first, second, last) -- cgit v1.2.1 From 19e0c67dacf51fea6560bac63903e14914268322 Mon Sep 17 00:00:00 2001 From: David Shea Date: Tue, 8 Jul 2014 08:42:22 -0400 Subject: Skip sequence index checks on types that do not resolve to an Instance or a Class. This avoids a crash on types that do not implement getattr, such as UnboundMethod. --- checkers/typecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 1869d5f..6a06b81 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -543,7 +543,7 @@ accessed. Python regular expressions are accepted.'} parent_type = safe_infer(node.parent.value) - if not parent_type: + if not isinstance(parent_type, (astroid.Class, astroid.Instance)): return # Determine what method on the parent this index will use -- cgit v1.2.1 From aa1e081cbba4c86e911c39e41249582f41b4a0c7 Mon Sep 17 00:00:00 2001 From: David Shea Date: Tue, 8 Jul 2014 08:43:04 -0400 Subject: Fix a typo in a comment --- checkers/typecheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 6a06b81..3080578 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -558,7 +558,7 @@ accessed. Python regular expressions are accepted.'} methodname = '__getitem__' # Check if this instance's __getitem__, __setitem__, or __delitem__, as - # appropriate to the statement, is implemented in a bulitin sequence + # appropriate to the statement, is implemented in a builtin sequence # type. This way we catch subclasses of sequence types but skip classes # that override __getitem__ and which may allow non-integer indices. try: -- cgit v1.2.1 From f496577ae6b172f7188a1196bbf0bdf865498485 Mon Sep 17 00:00:00 2001 From: David Shea Date: Tue, 8 Jul 2014 08:43:58 -0400 Subject: Skip the check for whether node is a Slice in visit_slice. It would actually be pretty weird if the node weren't a Slice in this case. --- checkers/typecheck.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 3080578..dfbce61 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -612,9 +612,6 @@ accessed. Python regular expressions are accepted.'} @check_messages('invalid-slice-index') def visit_slice(self, node): - if not isinstance(node, astroid.Slice): - return - # Check the type of each part of the slice for index in (node.lower, node.upper, node.step): if index is None: -- cgit v1.2.1 From bf5ee7f5b9f5258b8fc153aaf68ab5412ded336f Mon Sep 17 00:00:00 2001 From: David Shea Date: Tue, 8 Jul 2014 08:46:53 -0400 Subject: Move the sequence_types set to the global level --- checkers/typecheck.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/checkers/typecheck.py b/checkers/typecheck.py index dfbce61..3bcb099 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -86,6 +86,10 @@ MSGS = { with an __index__ method.'), } +# builtin sequence types in Python 2 and 3. +sequence_types = set(['str', 'unicode', 'list', 'tuple', 'bytearray', + 'xrange', 'range', 'bytes', 'memoryview']) + def _determine_callable(callable_obj): # Ordering is important, since BoundMethod is a subclass of UnboundMethod, # and Function inherits Lambda. @@ -538,9 +542,6 @@ accessed. Python regular expressions are accepted.'} # If the types can be determined, only allow indices to be int, # slice or instances with __index__. - sequence_types = set(['str', 'unicode', 'list', 'tuple', 'bytearray', - 'xrange', 'range', 'bytes', 'memoryview']) - parent_type = safe_infer(node.parent.value) if not isinstance(parent_type, (astroid.Class, astroid.Instance)): -- cgit v1.2.1 From 110ad5ce0e67d042dacde86d5b3150b7b3064dfc Mon Sep 17 00:00:00 2001 From: David Shea Date: Tue, 8 Jul 2014 09:04:39 -0400 Subject: Rearrange the sequence and slice index tests for better clarity --- test/input/func_invalid_sequence_index.py | 119 ++++++++++++++------------ test/input/func_invalid_slice_index.py | 6 +- test/messages/func_invalid_sequence_index.txt | 32 +++---- test/messages/func_invalid_slice_index.txt | 10 +-- 4 files changed, 90 insertions(+), 77 deletions(-) diff --git a/test/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py index 854eabe..b60e0b5 100644 --- a/test/input/func_invalid_sequence_index.py +++ b/test/input/func_invalid_sequence_index.py @@ -7,35 +7,60 @@ TESTLIST = [1, 2, 3] TESTTUPLE = (1, 2, 3) TESTSTR = '123' +# getitem tests with bad indices def function1(): """list index is a function""" return TESTLIST[id] def function2(): - """list index is a str constant""" - return TESTLIST['0'] - -def function3(): """list index is None""" return TESTLIST[None] -def function4(): +def function3(): """list index is a float expression""" return TESTLIST[float(0)] +def function4(): + """list index is a str constant""" + return TESTLIST['0'] + def function5(): + """list index does not implement __index__""" + class NonIndexType(object): + """Class without __index__ method""" + pass + + return TESTLIST[NonIndexType()] + +def function6(): + """Tuple index is None""" + return TESTTUPLE[None] + +def function7(): + """String index is None""" + return TESTSTR[None] + +def function8(): + """Index of subclass of tuple is None""" + class TupleTest(tuple): + """Subclass of tuple""" + pass + return TupleTest()[None] + +# getitem tests with good indices +def function9(): """list index is an int constant""" return TESTLIST[0] # no error -def function6(): +def function10(): """list index is a integer expression""" return TESTLIST[int(0.0)] # no error -def function7(): +def function11(): """list index is a slice""" return TESTLIST[slice(1, 2, 3)] # no error -def function8(): +def function12(): """list index implements __index__""" class IndexType(object): """Class with __index__ method""" @@ -45,7 +70,7 @@ def function8(): return TESTLIST[IndexType()] # no error -def function9(): +def function13(): """list index implements __index__ in a superclass""" class IndexType(object): """Class with __index__ method""" @@ -59,38 +84,14 @@ def function9(): return TESTLIST[IndexSubType()] # no error -def function10(): - """list index does not implement __index__""" - class NonIndexType(object): - """Class without __index__ method""" - pass - - return TESTLIST[NonIndexType()] - -# Repeat a handful of tests to ensure non-list types are caught -def function11(): - """Tuple index is None""" - return TESTTUPLE[None] - -def function12(): +def function14(): """Tuple index is an int constant""" return TESTTUPLE[0] -def function13(): - """String index is None""" - return TESTSTR[None] - -def function14(): +def function15(): """String index is an int constant""" return TESTSTR[0] -def function15(): - """Index of subclass of tuple is None""" - class TupleTest(tuple): - """Subclass of tuple""" - pass - return TupleTest()[None] - def function16(): """Index of subclass of tuple is an int constant""" class TupleTest(tuple): @@ -122,7 +123,6 @@ def function18(): return SubTupleTest()[None] # no error # Test with set and delete statements - def function19(): """Set with None and integer indices""" TESTLIST[None] = 0 @@ -139,9 +139,13 @@ def function21(): """Inherit all list get/set/del handlers""" pass test = ListTest() + + # Set and delete with invalid indices test[None] = 0 - test[0] = 0 # no error del test[None] + + # Set and delete with valid indices + test[0] = 0 # no error del test[0] # no error def function22(): @@ -151,12 +155,14 @@ def function22(): def __setitem__(self, key, value): pass test = ListTest() - test[None][0] = 0 - test[0][0] = 0 # no error - test[None] = 0 # no error - test[0] = 0 # no error + + test[None][0] = 0 # failure on the getitem with None del test[None] - del test[0] # no error + + test[0][0] = 0 # getitem with int and setitem with int, no error + test[None] = 0 # setitem overridden, no error + test[0] = 0 # setitem with int, no error + del test[0] # delitem with int, no error def function23(): """Get, set, and delete on a subclass of list that overrides __delitem__""" @@ -165,12 +171,14 @@ def function23(): def __delitem__(self, key): pass test = ListTest() - test[None][0] = 0 - test[0][0] = 0 # no error - test[None] = 0 - test[0] = 0 # no error - del test[None] # no error - del test[0] # no error + + test[None][0] = 0 # failure on the getitem with None + test[None] = 0 # setitem with invalid index + + test[0][0] = 0 # getitem with int and setitem with int, no error + test[0] = 0 # setitem with int, no error + del test[None] # delitem overriden, no error + del test[0] # delitem with int, no error def function24(): """Get, set, and delete on a subclass of list that overrides __getitem__""" @@ -179,13 +187,16 @@ def function24(): def __getitem__(self, key): pass test = ListTest() - test[None][0] = 0 # no error - test[0][0] = 0 # no error - test[None] = 0 - test[0] = 0 # no error - del test[None] - del test[0] # no error + test[None] = 0 # setitem with invalid index + del test[None] # delitem with invalid index + + test[None][0] = 0 # getitem overriden, no error + test[0][0] = 0 # getitem with int and setitem with int, no error + test[0] = 0 # setitem with int, no error + del test[0] # delitem with int, no error + +# Teest ExtSlice usage def function25(): """Extended slice used with a list""" return TESTLIST[..., 0] diff --git a/test/input/func_invalid_slice_index.py b/test/input/func_invalid_slice_index.py index fa72b8c..32f2f2d 100644 --- a/test/input/func_invalid_slice_index.py +++ b/test/input/func_invalid_slice_index.py @@ -5,6 +5,7 @@ __revision__ = 0 TESTLIST = [1, 2, 3] +# Invalid indices def function1(): """functions used as indices""" return TESTLIST[id:id:] @@ -22,13 +23,14 @@ def function3(): return TESTLIST[NoIndexTest()::] +# Valid indices def function4(): """integers used as indices""" return TESTLIST[0:0:0] # no error def function5(): """None used as indices""" - return TESTLIST[None:None:None] + return TESTLIST[None:None:None] # no error def function6(): """class with __index__ used as index""" @@ -56,4 +58,4 @@ def function7(): def function8(): """slice object used as index""" - return TESTLIST[slice(1, 2, 3)] + return TESTLIST[slice(1, 2, 3)] # no error diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt index a9b172a..db9edab 100644 --- a/test/messages/func_invalid_sequence_index.txt +++ b/test/messages/func_invalid_sequence_index.txt @@ -1,19 +1,19 @@ -E: 12:function1: Sequence index is not an int, slice, or instance with __index__ -E: 16:function2: Sequence index is not an int, slice, or instance with __index__ -E: 20:function3: Sequence index is not an int, slice, or instance with __index__ -E: 24:function4: Sequence index is not an int, slice, or instance with __index__ -E: 68:function10: Sequence index is not an int, slice, or instance with __index__ -E: 73:function11: Sequence index is not an int, slice, or instance with __index__ -E: 81:function13: Sequence index is not an int, slice, or instance with __index__ -E: 92:function15: Sequence index is not an int, slice, or instance with __index__ +E: 13:function1: Sequence index is not an int, slice, or instance with __index__ +E: 17:function2: Sequence index is not an int, slice, or instance with __index__ +E: 21:function3: Sequence index is not an int, slice, or instance with __index__ +E: 25:function4: Sequence index is not an int, slice, or instance with __index__ +E: 33:function5: Sequence index is not an int, slice, or instance with __index__ +E: 37:function6: Sequence index is not an int, slice, or instance with __index__ +E: 41:function7: Sequence index is not an int, slice, or instance with __index__ +E: 48:function8: Sequence index is not an int, slice, or instance with __index__ E:128:function19: Sequence index is not an int, slice, or instance with __index__ E:133:function20: Sequence index is not an int, slice, or instance with __index__ -E:142:function21: Sequence index is not an int, slice, or instance with __index__ E:144:function21: Sequence index is not an int, slice, or instance with __index__ -E:154:function22: Sequence index is not an int, slice, or instance with __index__ -E:158:function22: Sequence index is not an int, slice, or instance with __index__ -E:168:function23: Sequence index is not an int, slice, or instance with __index__ -E:170:function23: Sequence index is not an int, slice, or instance with __index__ -E:184:function24: Sequence index is not an int, slice, or instance with __index__ -E:186:function24: Sequence index is not an int, slice, or instance with __index__ -E:191:function25: Sequence index is not an int, slice, or instance with __index__ +E:145:function21: Sequence index is not an int, slice, or instance with __index__ +E:159:function22: Sequence index is not an int, slice, or instance with __index__ +E:160:function22: Sequence index is not an int, slice, or instance with __index__ +E:175:function23: Sequence index is not an int, slice, or instance with __index__ +E:176:function23: Sequence index is not an int, slice, or instance with __index__ +E:191:function24: Sequence index is not an int, slice, or instance with __index__ +E:192:function24: Sequence index is not an int, slice, or instance with __index__ +E:202:function25: Sequence index is not an int, slice, or instance with __index__ diff --git a/test/messages/func_invalid_slice_index.txt b/test/messages/func_invalid_slice_index.txt index d6882b1..d5b9e86 100644 --- a/test/messages/func_invalid_slice_index.txt +++ b/test/messages/func_invalid_slice_index.txt @@ -1,5 +1,5 @@ -E: 10:function1: Slice index is not an int, None, or instance with __index__ -E: 10:function1: Slice index is not an int, None, or instance with __index__ -E: 14:function2: Slice index is not an int, None, or instance with __index__ -E: 14:function2: Slice index is not an int, None, or instance with __index__ -E: 23:function3: Slice index is not an int, None, or instance with __index__ +E: 11:function1: Slice index is not an int, None, or instance with __index__ +E: 11:function1: Slice index is not an int, None, or instance with __index__ +E: 15:function2: Slice index is not an int, None, or instance with __index__ +E: 15:function2: Slice index is not an int, None, or instance with __index__ +E: 24:function3: Slice index is not an int, None, or instance with __index__ -- cgit v1.2.1 From 18ffa14174c836792209033fd89440db946dd41e Mon Sep 17 00:00:00 2001 From: cpopa Date: Tue, 8 Jul 2014 16:23:19 +0300 Subject: Give credit to recent contributor. Also, fix the news entry from the ChangeLog. --- CONTRIBUTORS.txt | 2 ++ ChangeLog | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 8b28ae3..eeb582d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -32,6 +32,8 @@ Order doesn't matter (not that much, at least ;) * Nathaniel Manista: suspicious lambda checking +* David Shea: invalid sequence and slice index + * Wolfgang Grafen, Axel Muller, Fabio Zadrozny, Pierre Rouleau, Maarten ter Huurne, Mirko Friedenhagen and all the Logilab's team (among others): bug reports, feedback, feature requests... Many other people have contributed diff --git a/ChangeLog b/ChangeLog index 59d549e..3a51fca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -23,7 +23,8 @@ ChangeLog for Pylint * Fix a false positive with unbalanced iterable unpacking, when encountering starred nodes. Closes issue #273. - * Add checks for invalid sequence and slice indices + * Add new checks, 'invalid-slice-index' and 'invalid-sequence-index' + for invalid sequence and slice indices. 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the -- cgit v1.2.1 From 3be19d82c2a1142063c691efb3443ff74c2aba28 Mon Sep 17 00:00:00 2001 From: Alexandru Coman Date: Wed, 9 Jul 2014 01:44:35 +0300 Subject: Fixing Issue #149 (W0511 false positive) --- checkers/misc.py | 32 ++++++++++++++++++-------------- test/input/func_fixme.py | 6 ++++-- test/messages/func_fixme.txt | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/checkers/misc.py b/checkers/misc.py index d1b7c21..45ed348 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -32,10 +32,11 @@ MSGS = { 'Used when a source line cannot be decoded using the specified ' 'source file encoding.', {'maxversion': (3, 0)}), - } +} class EncodingChecker(BaseChecker): + """checks for: * warning notes in the code like FIXME, XXX * encoding issues. @@ -47,17 +48,17 @@ class EncodingChecker(BaseChecker): msgs = MSGS options = (('notes', - {'type' : 'csv', 'metavar' : '', - 'default' : ('FIXME', 'XXX', 'TODO'), - 'help' : 'List of note tags to take in consideration, \ -separated by a comma.' - }), - ) + {'type': 'csv', 'metavar': '', + 'default': ('FIXME', 'XXX', 'TODO'), + 'help': ('List of note tags to take in consideration, ' + 'separated by a comma.')}),) def _check_note(self, notes, lineno, line): - match = notes.search(line) - if match: - self.add_message('fixme', args=line[match.start():-1], line=lineno) + for note in notes: + match = note.search(line) + if not match: + continue + self.add_message('fixme', args=match.group(1).strip(), line=lineno) def _check_encoding(self, lineno, line, file_encoding): try: @@ -71,19 +72,22 @@ separated by a comma.' notes """ stream = module.file_stream - stream.seek(0) # XXX may be removed with astroid > 0.23 + stream.seek(0) # XXX may be removed with astroid > 0.23 if self.config.notes: - notes = re.compile('|'.join(self.config.notes)) + notes = [re.compile(r'.*?#\s+({}:*\s*.+)'.format(note)) + for note in self.config.notes] else: notes = None if module.file_encoding: encoding = module.file_encoding else: encoding = 'ascii' + for lineno, line in enumerate(stream): - line = self._check_encoding(lineno+1, line, encoding) + line = self._check_encoding(lineno + 1, line, encoding) if line is not None and notes: - self._check_note(notes, lineno+1, line) + self._check_note(notes, lineno + 1, line) + def register(linter): """required method to auto register this checker""" diff --git a/test/input/func_fixme.py b/test/input/func_fixme.py index b6371f1..7bd6653 100644 --- a/test/input/func_fixme.py +++ b/test/input/func_fixme.py @@ -1,9 +1,11 @@ """docstring""" - +# pylint: disable=W0612 __revision__ = '' # FIXME: beep + def function(): '''XXX:bop''' - + variable = "FIXME: Ignore me!" + test = "text" # FIXME: Valid test diff --git a/test/messages/func_fixme.txt b/test/messages/func_fixme.txt index 2544ce8..c114e79 100644 --- a/test/messages/func_fixme.txt +++ b/test/messages/func_fixme.txt @@ -1,2 +1,2 @@ W: 5: FIXME: beep -W: 8: XXX:bop''' +W: 11: FIXME: Valid test \ No newline at end of file -- cgit v1.2.1 From 301c7fab2effb2a3ea440e88d5e3e9c1509cd487 Mon Sep 17 00:00:00 2001 From: Alexandru Coman Date: Wed, 9 Jul 2014 11:06:37 +0300 Subject: Code refactoring. --- checkers/misc.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/checkers/misc.py b/checkers/misc.py index 45ed348..3afe63b 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -54,11 +54,10 @@ class EncodingChecker(BaseChecker): 'separated by a comma.')}),) def _check_note(self, notes, lineno, line): - for note in notes: - match = note.search(line) - if not match: - continue - self.add_message('fixme', args=match.group(1).strip(), line=lineno) + match = notes.search(line) + if not match: + return + self.add_message('fixme', args=line[match.start(1):-1], line=lineno) def _check_encoding(self, lineno, line, file_encoding): try: @@ -74,8 +73,8 @@ class EncodingChecker(BaseChecker): stream = module.file_stream stream.seek(0) # XXX may be removed with astroid > 0.23 if self.config.notes: - notes = [re.compile(r'.*?#\s+({}:*\s*.+)'.format(note)) - for note in self.config.notes] + notes = re.compile(r'.*?#\s+({})(:*\s*.+)'.format( + "|".join(self.config.notes))) else: notes = None if module.file_encoding: -- cgit v1.2.1 From f2fd6266e5bcf4a1f5c268c144ce0aff9529e51a Mon Sep 17 00:00:00 2001 From: Alexandru Coman Date: Wed, 9 Jul 2014 11:26:27 +0300 Subject: Fix a false positive regarding W0511. Closes issue #149. --- ChangeLog | 1 + checkers/misc.py | 4 ++-- test/input/func_fixme.py | 3 +++ test/messages/func_fixme.txt | 4 +++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 3a51fca..fe71205 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,7 @@ ChangeLog for Pylint ==================== -- + * Fix a false positive regarding W0511. Closes issue #149. * Fix unused-import false positive with Python 3 metaclasses (#143). * Don't warn with 'bad-format-character' when encountering diff --git a/checkers/misc.py b/checkers/misc.py index 3afe63b..b4738b0 100644 --- a/checkers/misc.py +++ b/checkers/misc.py @@ -73,8 +73,8 @@ class EncodingChecker(BaseChecker): stream = module.file_stream stream.seek(0) # XXX may be removed with astroid > 0.23 if self.config.notes: - notes = re.compile(r'.*?#\s+({})(:*\s*.+)'.format( - "|".join(self.config.notes))) + notes = re.compile( + r'.*?#\s+(%s)(:*\s*.+)' % "|".join(self.config.notes)) else: notes = None if module.file_encoding: diff --git a/test/input/func_fixme.py b/test/input/func_fixme.py index 7bd6653..66e86db 100644 --- a/test/input/func_fixme.py +++ b/test/input/func_fixme.py @@ -9,3 +9,6 @@ def function(): '''XXX:bop''' variable = "FIXME: Ignore me!" test = "text" # FIXME: Valid test + + # TODO: Do something with the variables + xxx = "n/a" # XXX: Fix this later diff --git a/test/messages/func_fixme.txt b/test/messages/func_fixme.txt index c114e79..88199a7 100644 --- a/test/messages/func_fixme.txt +++ b/test/messages/func_fixme.txt @@ -1,2 +1,4 @@ W: 5: FIXME: beep -W: 11: FIXME: Valid test \ No newline at end of file +W: 11: FIXME: Valid test +W: 13: TODO: Do something with the variables +W: 14: XXX: Fix this later \ No newline at end of file -- cgit v1.2.1 From 233952109c5e9c5aecba9525646afdcbe7b34bdd Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 10 Jul 2014 13:27:20 +0300 Subject: Add 'assigning-non-slot' warning, which detects assignments to attributes not defined in slots. --- ChangeLog | 4 +++ checkers/classes.py | 33 +++++++++++++++++- test/input/func_assigning_non_slot.py | 56 +++++++++++++++++++++++++++++++ test/messages/func_assigning_non_slot.txt | 3 ++ 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 test/input/func_assigning_non_slot.py create mode 100644 test/messages/func_assigning_non_slot.txt diff --git a/ChangeLog b/ChangeLog index fe71205..a977dae 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ ChangeLog for Pylint -- * Fix a false positive regarding W0511. Closes issue #149. + * Fix unused-import false positive with Python 3 metaclasses (#143). * Don't warn with 'bad-format-character' when encountering @@ -27,6 +28,9 @@ ChangeLog for Pylint * Add new checks, 'invalid-slice-index' and 'invalid-sequence-index' for invalid sequence and slice indices. + * Add 'assigning-non-slot' warning, which detects assignments to + attributes not defined in slots. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/classes.py b/checkers/classes.py index e550734..570f7ac 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -26,7 +26,8 @@ from astroid.bases import Generator from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker from pylint.checkers.utils import (PYMETHODS, overrides_a_method, - check_messages, is_attr_private, is_attr_protected, node_frame_class) + check_messages, is_attr_private, is_attr_protected, node_frame_class, + safe_infer) if sys.version_info >= (3, 0): NEXT_METHOD = '__next__' @@ -164,6 +165,10 @@ MSGS = { 'only non empty strings', 'invalid-slots-object', 'Used when an invalid (non-string) object occurs in __slots__.'), + 'E0237': ('Assigning to attribute %r not defined in class slots', + 'assigning-non-slot', + 'Used when assigning to an attribute not defined ' + 'in the class slots.'), 'E0238': ('Invalid __slots__ object', 'invalid-slots', 'Used when an invalid __slots__ is found in class. ' @@ -463,6 +468,32 @@ a metaclass class method.'} def visit_assattr(self, node): if isinstance(node.ass_type(), astroid.AugAssign) and self.is_first_attr(node): self._accessed[-1].setdefault(node.attrname, []).append(node) + self._check_in_slots(node) + + def _check_in_slots(self, node): + """ Check that the given assattr node + is defined in the class slots. + """ + infered = safe_infer(node.expr) + if infered and isinstance(infered, Instance): + klass = infered._proxied + if '__slots__' not in klass.locals or not klass.newstyle: + return + + slots = klass.slots() + # If any ancestor doesn't use slots, the slots + # defined for this class are superfluous. + if any('__slots__' not in ancestor.locals and + ancestor.name != 'object' + for ancestor in klass.ancestors()): + return + + if not any(slot.value == node.attrname for slot in slots): + # If we have a '__dict__' in slots, then + # assigning any name is valid. + if not any(slot.value == '__dict__' for slot in slots): + self.add_message('assigning-non-slot', + args=(node.attrname, ), node=node) @check_messages('protected-access') def visit_assign(self, assign_node): diff --git a/test/input/func_assigning_non_slot.py b/test/input/func_assigning_non_slot.py new file mode 100644 index 0000000..81e241c --- /dev/null +++ b/test/input/func_assigning_non_slot.py @@ -0,0 +1,56 @@ +""" Checks assigning attributes not found in class slots +will trigger assigning-non-slot warning. +""" +# pylint: disable=too-few-public-methods, no-init +from collections import deque + +__revision__ = 0 + +class Empty: + """ empty """ + +class Bad(object): + """ missing not in slots. """ + + __slots__ = ['member'] + + def __init__(self): + self.missing = 42 + +class Bad2(object): + """ missing not in slots """ + __slots__ = [deque.__name__, 'member'] + + def __init__(self): + self.deque = 42 + self.missing = 42 + +class Bad3(Bad): + """ missing not found in slots """ + + __slots__ = ['component'] + + def __init__(self): + self.component = 42 + self.member = 24 + self.missing = 42 + super(Bad3, self).__init__() + +class Good(Empty): + """ missing not in slots, but Empty doesn't + specify __slots__. + """ + __slots__ = ['a'] + + def __init__(self): + self.missing = 42 + +class Good2(object): + """ Using __dict__ in slots will be safe. """ + + __slots__ = ['__dict__', 'comp'] + + def __init__(self): + self.comp = 4 + self.missing = 5 + \ No newline at end of file diff --git a/test/messages/func_assigning_non_slot.txt b/test/messages/func_assigning_non_slot.txt new file mode 100644 index 0000000..5a06fc6 --- /dev/null +++ b/test/messages/func_assigning_non_slot.txt @@ -0,0 +1,3 @@ +E: 18:Bad.__init__: Assigning to attribute 'missing' not defined in class slots +E: 26:Bad2.__init__: Assigning to attribute 'missing' not defined in class slots +E: 36:Bad3.__init__: Assigning to attribute 'missing' not defined in class slots \ No newline at end of file -- cgit v1.2.1 From 999022747dac712e3b9155b35126599d1b3b748c Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 10 Jul 2014 13:28:53 +0300 Subject: Fix test for Python 2. --- test/input/func_assigning_non_slot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/input/func_assigning_non_slot.py b/test/input/func_assigning_non_slot.py index 81e241c..9c91dd2 100644 --- a/test/input/func_assigning_non_slot.py +++ b/test/input/func_assigning_non_slot.py @@ -6,7 +6,7 @@ from collections import deque __revision__ = 0 -class Empty: +class Empty(object): """ empty """ class Bad(object): -- cgit v1.2.1 From 35f3bb113315426cdf32ea7af284af897f19d017 Mon Sep 17 00:00:00 2001 From: Alexandru Coman Date: Fri, 11 Jul 2014 02:05:34 +0300 Subject: Checkers respect priority now. Close issue #229. --- ChangeLog | 1 + lint.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index a977dae..44dc584 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,7 @@ ChangeLog for Pylint ==================== -- + * Checkers respect priority now. Close issue #229. * Fix a false positive regarding W0511. Closes issue #149. * Fix unused-import false positive with Python 3 metaclasses (#143). diff --git a/lint.py b/lint.py index 529fbd4..7816e3b 100644 --- a/lint.py +++ b/lint.py @@ -33,6 +33,7 @@ import functools import sys import os import tokenize +from operator import attrgetter from warnings import warn from logilab.common.configuration import UnsupportedAction, OptionsManagerMixIn @@ -569,9 +570,13 @@ warning, statement which respectively contain the number of errors / warnings\ if msg[0] != 'F' and self.is_message_enabled(msg)) if (messages or any(self.report_is_enabled(r[0]) for r in checker.reports)): - neededcheckers.append(checker) + neededcheckers.append(checker) + # Sort checkers by priority + neededcheckers = sorted(neededcheckers, key=attrgetter('priority'), + reverse=True) return neededcheckers + def should_analyze_file(self, modname, path): # pylint: disable=unused-argument """Returns whether or not a module should be checked. -- cgit v1.2.1 From 738cfea5cdc05ee4a1cc7914188d507dfebcfea3 Mon Sep 17 00:00:00 2001 From: Alexandru Coman Date: Fri, 11 Jul 2014 02:09:59 +0300 Subject: Remove extra spaces. --- ChangeLog | 1 + lint.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 44dc584..fe2f7ae 100644 --- a/ChangeLog +++ b/ChangeLog @@ -3,6 +3,7 @@ ChangeLog for Pylint -- * Checkers respect priority now. Close issue #229. + * Fix a false positive regarding W0511. Closes issue #149. * Fix unused-import false positive with Python 3 metaclasses (#143). diff --git a/lint.py b/lint.py index 7816e3b..26e099d 100644 --- a/lint.py +++ b/lint.py @@ -576,7 +576,6 @@ warning, statement which respectively contain the number of errors / warnings\ reverse=True) return neededcheckers - def should_analyze_file(self, modname, path): # pylint: disable=unused-argument """Returns whether or not a module should be checked. -- cgit v1.2.1 From 235ee6dd14de0678d4f219bd57624c10af816cd0 Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 12 Jul 2014 12:08:41 +0300 Subject: Don't emit 'no-name-in-module' for ignored modules. Closes issue #223. --- ChangeLog | 3 +++ checkers/variables.py | 8 +++++++- test/unittest_checker_variables.py | 15 ++++++++++++++- testutils.py | 3 +++ utils.py | 20 ++++++++++++++++++++ 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index fe2f7ae..0e005c4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -33,6 +33,9 @@ ChangeLog for Pylint * Add 'assigning-non-slot' warning, which detects assignments to attributes not defined in slots. + * Don't emit 'no-name-in-module' for ignored modules. + Closes issue #223. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/variables.py b/checkers/variables.py index b84ad4f..6cbcdb6 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -25,6 +25,7 @@ from astroid import are_exclusive, builtin_lookup, AstroidBuildingException from logilab.common.modutils import file_from_modpath from pylint.interfaces import IAstroidChecker +from pylint.utils import get_global_option from pylint.checkers import BaseChecker from pylint.checkers.utils import (PYMETHODS, is_ancestor_name, is_builtin, is_defined_before, is_error, is_func_default, is_func_decorator, @@ -679,6 +680,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' if the latest access name corresponds to a module, return it """ assert isinstance(module, astroid.Module), module + ignored_modules = get_global_option(self, 'ignored-modules', + default=[]) while module_names: name = module_names.pop(0) if name == '__dict__': @@ -689,7 +692,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' if module is astroid.YES: return None except astroid.NotFoundError: - self.add_message('no-name-in-module', args=(name, module.name), node=node) + if module.name in ignored_modules: + return None + self.add_message('no-name-in-module', + args=(name, module.name), node=node) return None except astroid.InferenceError: return None diff --git a/test/unittest_checker_variables.py b/test/unittest_checker_variables.py index 30bc5e6..20a0d9e 100644 --- a/test/unittest_checker_variables.py +++ b/test/unittest_checker_variables.py @@ -4,7 +4,7 @@ import os from astroid import test_utils from pylint.checkers import variables -from pylint.testutils import CheckerTestCase, linter +from pylint.testutils import CheckerTestCase, linter, set_config class VariablesCheckerTC(CheckerTestCase): @@ -22,6 +22,19 @@ class VariablesCheckerTC(CheckerTestCase): with self.assertNoMessages(): self.walk(module) + @set_config(ignored_modules=('argparse',)) + def test_no_name_in_module_skipped(self): + """Make sure that 'from ... import ...' does not emit a + 'no-name-in-module' with a module that is configured + to be ignored. + """ + + node = test_utils.extract_node(""" + from argparse import THIS_does_not_EXIST + """) + with self.assertNoMessages(): + self.checker.visit_from(node) + class MissingSubmoduleTest(CheckerTestCase): CHECKER_CLASS = variables.VariablesChecker diff --git a/testutils.py b/testutils.py index fb8170a..daf7477 100644 --- a/testutils.py +++ b/testutils.py @@ -152,6 +152,9 @@ class UnittestLinter(object): self.stats[name] = value return self.stats + @property + def options_providers(self): + return linter.options_providers def set_config(**kwargs): """Decorator for setting config values on a checker.""" diff --git a/utils.py b/utils.py index c6a7371..c2f1704 100644 --- a/utils.py +++ b/utils.py @@ -742,3 +742,23 @@ def register_plugins(linter, directory): module.register(linter) imported[base] = 1 +def get_global_option(checker, option, default=None): + """ Retrieve an option defined by the given *checker* or + by all known option providers. + + It will look in the list of all options providers + until the given *option* will be found. + If the option wasn't found, the *default* value will be returned. + """ + # First, try in the given checker's config. + # After that, look in the options providers. + + try: + return getattr(checker.config, option.replace("-", "_")) + except AttributeError: + pass + for provider in checker.linter.options_providers: + for options in provider.options: + if options[0] == option: + return getattr(provider.config, option.replace("-", "_")) + return default -- cgit v1.2.1 From d61747ad0031f10f06145cc848d6d13c089f292f Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 17 Jul 2014 23:31:24 +0300 Subject: Fix an 'unused-variable' false positive, where the variable is assigned through an import. Closes issue #196. --- ChangeLog | 3 +++ checkers/variables.py | 42 +++++++++++++++++++++++++++++++++++++++++- test/input/func_w0612.py | 18 +++++++++++++++++- test/messages/func_w0612.txt | 8 ++++++-- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0e005c4..390b374 100644 --- a/ChangeLog +++ b/ChangeLog @@ -36,6 +36,9 @@ ChangeLog for Pylint * Don't emit 'no-name-in-module' for ignored modules. Closes issue #223. + * Fix an 'unused-variable' false positive, where the variable is + assigned through an import. Closes issue #196. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/variables.py b/checkers/variables.py index 6cbcdb6..94a8d6e 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -356,6 +356,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' authorized_rgx = self.config.dummy_variables_rgx called_overridden = False argnames = node.argnames() + global_names = set() + for global_stmt in node.nodes_of_class(astroid.Global): + global_names.update(set(global_stmt.names)) + for name, stmts in not_consumed.iteritems(): # ignore some special names specified by user configuration if authorized_rgx.match(name): @@ -365,6 +369,23 @@ builtins. Remember that you should avoid to define new builtins when possible.' stmt = stmts[0] if isinstance(stmt, astroid.Global): continue + if isinstance(stmt, (astroid.Import, astroid.From)): + # Detect imports, assigned to global statements. + if global_names: + skip = False + for import_name, import_alias in stmt.names: + # If the import uses an alias, check only that. + # Otherwise, check only the import name. + if import_alias: + if import_alias in global_names: + skip = True + break + elif import_name in global_names: + skip = True + break + if skip: + continue + # care about functions with unknown argument (builtins) if name in argnames: if is_method: @@ -412,7 +433,26 @@ builtins. Remember that you should avoid to define new builtins when possible.' break else: # global but no assignment - self.add_message('global-variable-not-assigned', args=name, node=node) + # Detect imports in the current frame, with the required + # name. Such imports can be considered assignments. + imports = frame.nodes_of_class((astroid.Import, astroid.From)) + for import_node in imports: + found = False + for import_name, import_alias in import_node.names: + # If the import uses an alias, check only that. + # Otherwise, check only the import name. + if import_alias: + if import_alias == name: + found = True + break + elif import_name and import_name == name: + found = True + break + if found: + break + else: + self.add_message('global-variable-not-assigned', + args=name, node=node) default_message = False if not assign_nodes: continue diff --git a/test/input/func_w0612.py b/test/input/func_w0612.py index 57e139c..e871bb2 100644 --- a/test/input/func_w0612.py +++ b/test/input/func_w0612.py @@ -1,7 +1,8 @@ """test unused variable """ - +# pylint: disable=invalid-name, redefined-outer-name __revision__ = 0 +PATH = OS = collections = deque = None def function(matches): """"yo""" @@ -20,3 +21,18 @@ def visit_if(self, node): self.inc_branch(branches) self.stmts += branches +def test_global(): + """ Test various assignments of global + variables through imports. + """ + global PATH, OS, collections, deque + from os import path as PATH + import os as OS + import collections + from collections import deque + # make sure that these triggers unused-variable + from sys import platform + from sys import version as VERSION + import this + import re as RE + diff --git a/test/messages/func_w0612.txt b/test/messages/func_w0612.txt index f1647a3..c81b4f9 100644 --- a/test/messages/func_w0612.txt +++ b/test/messages/func_w0612.txt @@ -1,2 +1,6 @@ -W: 8:function: Unused variable 'aaaa' -W: 9:function: Unused variable 'index' +W: 9:function: Unused variable 'aaaa' +W: 10:function: Unused variable 'index' +W: 34:test_global: Unused variable 'platform' +W: 35:test_global: Unused variable 'VERSION' +W: 36:test_global: Unused variable 'this' +W: 37:test_global: Unused variable 'RE' -- cgit v1.2.1 From d2dac2c5d2ba2f8a06fe43d815fd3ff63a089000 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 21 Jul 2014 21:13:04 +0200 Subject: Definition order is considered for classes, function arguments and annotations. Closes issue #257. --- ChangeLog | 3 + checkers/variables.py | 67 +++++++++++++++++++++- test/input/func_undefined_var.py | 36 +++++++++++- test/input/func_used_before_assignment_py30.py | 22 ++++++- test/messages/func_undefined_var.txt | 4 ++ test/messages/func_used_before_assignment_py30.txt | 6 +- 6 files changed, 134 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index 390b374..27b7380 100644 --- a/ChangeLog +++ b/ChangeLog @@ -39,6 +39,9 @@ ChangeLog for Pylint * Fix an 'unused-variable' false positive, where the variable is assigned through an import. Closes issue #196. + * Definition order is considered for classes, function arguments + and annotations. Closes issue #257. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/variables.py b/checkers/variables.py index 94a8d6e..29f9529 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -69,6 +69,71 @@ def _get_unpacking_extra_info(node, infered): more = ' defined at line %s of %s' % (infered.lineno, infered_module) return more +def _detect_global_scope(node, frame, defframe): + """ Detect that the given frames shares a global + scope. + + Two frames shares a global scope when neither + of them are hidden under a function scope, as well + as any of parent scope of them, until the root scope. + In this case, depending from something defined later on + will not work, because it is still undefined. + + Example: + class A: + # B has the same global scope as `C`, leading to a NameError. + class B(C): ... + class C: ... + + """ + def_scope = scope = None + if frame and frame.parent: + scope = frame.parent.scope() + if defframe and defframe.parent: + def_scope = defframe.parent.scope() + if isinstance(frame, astroid.Function): + # If the parent of the current node is a + # function, then it can be under its scope + # (defined in, which doesn't concern us) or + # the `->` part of annotations. The same goes + # for annotations of function arguments, they'll have + # their parent the Arguments node. + if not isinstance(node.parent, + (astroid.Function, astroid.Arguments)): + return False + elif any(not isinstance(f, (astroid.Class, astroid.Module)) + for f in (frame, defframe)): + # Not interested in other frames, since they are already + # not in a global scope. + return False + + break_scopes = [] + for s in (scope, def_scope): + # Look for parent scopes. If there is anything different + # than a module or a class scope, then they frames don't + # share a global scope. + parent_scope = s + while parent_scope: + if not isinstance(parent_scope, (astroid.Class, astroid.Module)): + break_scopes.append(parent_scope) + break + if parent_scope.parent: + parent_scope = parent_scope.parent.scope() + else: + break + if break_scopes and len(set(break_scopes)) != 1: + # Store different scopes than expected. + # If the stored scopes are, in fact, the very same, then it means + # that the two frames (frame and defframe) shares the same scope, + # and we could apply our lineno analysis over them. + # For instance, this works when they are inside a function, the node + # that uses a definition and the definition itself. + return False + # At this point, we are certain that frame and defframe shares a scope + # and the definition of the first depends on the second. + return frame.lineno < defframe.lineno + + MSGS = { 'E0601': ('Using variable %r before assignment', 'used-before-assignment', @@ -593,7 +658,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' defframe = defstmt.frame() maybee0601 = True if not frame is defframe: - maybee0601 = False + maybee0601 = _detect_global_scope(node, frame, defframe) elif defframe.parent is None: # we are at the module level, check the name is not # defined in builtins diff --git a/test/input/func_undefined_var.py b/test/input/func_undefined_var.py index 407f3f6..fb5fc30 100644 --- a/test/input/func_undefined_var.py +++ b/test/input/func_undefined_var.py @@ -1,5 +1,5 @@ """test access to undefined variables""" - +# pylint: disable=too-few-public-methods, no-init, no-self-use __revision__ = '$Id:' DEFINED = 1 @@ -83,3 +83,37 @@ def func1(): def func2(): """A function with a decorator that contains a genexpr.""" pass + +# Test shared scope. + +def test_arguments(arg=TestClass): + """ TestClass isn't defined yet. """ + return arg + +class TestClass(Ancestor): + """ contains another class, which uses an undefined ancestor. """ + + class MissingAncestor(Ancestor1): + """ no op """ + + def test1(self): + """ It should trigger here, because the two classes + have the same scope. + """ + class UsingBeforeDefinition(Empty): + """ uses Empty before definition """ + class Empty(object): + """ no op """ + return UsingBeforeDefinition + + def test(self): + """ Ancestor isn't defined yet, but we don't care. """ + class MissingAncestor1(Ancestor): + """ no op """ + return MissingAncestor1 + +class Ancestor(object): + """ No op """ + +class Ancestor1(object): + """ No op """ diff --git a/test/input/func_used_before_assignment_py30.py b/test/input/func_used_before_assignment_py30.py index b5d0bf3..ae979a1 100644 --- a/test/input/func_used_before_assignment_py30.py +++ b/test/input/func_used_before_assignment_py30.py @@ -1,5 +1,5 @@ """Check for nonlocal and used-before-assignment""" -# pylint: disable=missing-docstring, unused-variable +# pylint: disable=missing-docstring, unused-variable, no-init, too-few-public-methods __revision__ = 0 @@ -26,3 +26,23 @@ def test_fail2(): nonlocal count cnt = cnt + 1 wrap() + +def test_fail3(arg: test_fail4): + """ Depends on `test_fail4`, in argument annotation. """ + return arg + +def test_fail4(*args: test_fail5, **kwargs: undefined): + """ Depends on `test_fail5` and `undefined` in + variable and named arguments annotations. + """ + return args, kwargs + +def test_fail5()->undefined1: + """ Depends on `undefined1` in function return annotation. """ + +def undefined(): + """ no op """ + +def undefined1(): + """ no op """ + diff --git a/test/messages/func_undefined_var.txt b/test/messages/func_undefined_var.txt index 25fb2c3..5505156 100644 --- a/test/messages/func_undefined_var.txt +++ b/test/messages/func_undefined_var.txt @@ -7,4 +7,8 @@ E: 27:bad_default: Undefined variable 'augvar' E: 28:bad_default: Undefined variable 'vardel' E: 56: Using variable 'PLOUF' before assignment E: 65:if_branch_test: Using variable 'xxx' before assignment +E: 89:test_arguments: Using variable 'TestClass' before assignment +E: 93:TestClass: Using variable 'Ancestor' before assignment +E: 96:TestClass.MissingAncestor: Using variable 'Ancestor1' before assignment +E:103:TestClass.test1.UsingBeforeDefinition: Using variable 'Empty' before assignment W: 27:bad_default: Unused variable 'augvar' diff --git a/test/messages/func_used_before_assignment_py30.txt b/test/messages/func_used_before_assignment_py30.txt index 5b6080f..8bb131d 100644 --- a/test/messages/func_used_before_assignment_py30.txt +++ b/test/messages/func_used_before_assignment_py30.txt @@ -1,2 +1,6 @@ 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 +E: 27:test_fail2.wrap: Using variable 'cnt' before assignment +E: 30:test_fail3: Using variable 'test_fail4' before assignment +E: 34:test_fail4: Using variable 'test_fail5' before assignment +E: 34:test_fail4: Using variable 'undefined' before assignment +E: 40:test_fail5: Using variable 'undefined1' before assignment \ No newline at end of file -- cgit v1.2.1 From f12378d90b206278b704624fadcdcbea15d13463 Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 23 Jul 2014 09:25:37 +0200 Subject: Don't use set comprehension. --- checkers/strings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index 4fe16dd..ad63580 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -327,10 +327,10 @@ class StringMethodsChecker(BaseChecker): self.add_message('bad-format-string', node=node) return - manual_fields = {field[0] for field in fields - if isinstance(field[0], int)} - named_fields = {field[0] for field in fields - if isinstance(field[0], str)} + manual_fields = set(field[0] for field in fields + if isinstance(field[0], int)) + named_fields = set(field[0] for field in fields + if isinstance(field[0], str)) if manual_fields and num_args: self.add_message('format-combined-specification', node=node) -- cgit v1.2.1 From 228cf2146e6485e34e78b73570ea0a5a5ec05e75 Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 23 Jul 2014 12:03:12 +0200 Subject: Remove check_messages, so that leave_module can be called if 'undefined-variable' is disabled. --- checkers/variables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/checkers/variables.py b/checkers/variables.py index 6fba97d..aa63177 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -831,7 +831,6 @@ class VariablesChecker3k(VariablesChecker): # do not check for not used locals here self._to_consume.pop() - @check_messages('undefined-variable') def leave_module(self, node): """ Update consumption analysis variable for metaclasses. -- cgit v1.2.1 From 4bdb95ad8d3d14fd419119c7ae379da3bddcb126 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 23 Jul 2014 12:36:42 +0200 Subject: stop having ASTWalker as BaseChecker base class, it's not used anymore (and for a while) --- checkers/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/checkers/__init__.py b/checkers/__init__.py index af7965b..693a5ff 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -42,7 +42,6 @@ import sys import tokenize import warnings -from astroid.utils import ASTWalker from logilab.common.configuration import OptionsProviderMixIn from pylint.reporters import diff_string @@ -69,7 +68,7 @@ def table_lines_from_stats(stats, old_stats, columns): return lines -class BaseChecker(OptionsProviderMixIn, ASTWalker): +class BaseChecker(OptionsProviderMixIn): """base class for checkers""" # checker name (you may reuse an existing one) name = None @@ -87,7 +86,6 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker): linter is an object implementing ILinter """ - ASTWalker.__init__(self, self) self.name = self.name.lower() OptionsProviderMixIn.__init__(self) self.linter = linter -- cgit v1.2.1 From 1df3bf598b0f5256802cb1d5486be3bffde20632 Mon Sep 17 00:00:00 2001 From: cpopa Date: Wed, 23 Jul 2014 13:17:29 +0200 Subject: Don't emit 'unused-variable' when assigning to a nonlocal. Closes issue #275. --- ChangeLog | 3 +++ checkers/variables.py | 6 ++++++ test/input/func_noerror_unused_variable_py30.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 test/input/func_noerror_unused_variable_py30.py diff --git a/ChangeLog b/ChangeLog index 27b7380..0c8add8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -42,6 +42,9 @@ ChangeLog for Pylint * Definition order is considered for classes, function arguments and annotations. Closes issue #257. + * Don't emit 'unused-variable' when assigning to a nonlocal. + Closes issue #275. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the configuration file, which was accidentally broken in 1.2.0. diff --git a/checkers/variables.py b/checkers/variables.py index 29f9529..4150706 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -422,8 +422,11 @@ builtins. Remember that you should avoid to define new builtins when possible.' called_overridden = False argnames = node.argnames() global_names = set() + nonlocal_names = set() for global_stmt in node.nodes_of_class(astroid.Global): global_names.update(set(global_stmt.names)) + for nonlocal_stmt in node.nodes_of_class(astroid.Nonlocal): + nonlocal_names.update(set(nonlocal_stmt.names)) for name, stmts in not_consumed.iteritems(): # ignore some special names specified by user configuration @@ -470,6 +473,9 @@ builtins. Remember that you should avoid to define new builtins when possible.' continue self.add_message('unused-argument', args=name, node=stmt) else: + if stmt.parent and isinstance(stmt.parent, astroid.Assign): + if name in nonlocal_names: + continue self.add_message('unused-variable', args=name, node=stmt) @check_messages('global-variable-undefined', 'global-variable-not-assigned', 'global-statement', diff --git a/test/input/func_noerror_unused_variable_py30.py b/test/input/func_noerror_unused_variable_py30.py new file mode 100644 index 0000000..ffcc978 --- /dev/null +++ b/test/input/func_noerror_unused_variable_py30.py @@ -0,0 +1,14 @@ +""" Test nonlocal uses and unused-variable. """ + +__revision__ = 1 + +def test_nonlocal(): + """ Test that assigning to a nonlocal does not trigger + an 'unused-variable' warnings. + """ + attr = True + def set_value(val): + """ Set the value in a nonlocal. """ + nonlocal attr + attr = val + return set_value -- cgit v1.2.1 From c141ccdb55553bc35d38874826a0304df7f96596 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 23 Jul 2014 17:43:52 +0200 Subject: import modutils from astroid, it has been backported there --- __pkginfo__.py | 4 ++-- checkers/imports.py | 2 +- debian/control | 2 +- lint.py | 2 +- utils.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/__pkginfo__.py b/__pkginfo__.py index 86488fa..2e2008b 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -23,10 +23,10 @@ numversion = (1, 2, 1) version = '.'.join([str(num) for num in numversion]) if sys.version_info < (2, 6): - install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1', + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.2', 'StringFormat'] else: - install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1'] + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.2'] license = 'GPL' description = "python code static checker" diff --git a/checkers/imports.py b/checkers/imports.py index 8b73c6f..46ed8ac 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -18,11 +18,11 @@ import sys from logilab.common.graph import get_cycles, DotBackend -from logilab.common.modutils import get_module_part, is_standard_module from logilab.common.ureports import VerbatimText, Paragraph import astroid from astroid import are_exclusive +from astroid.modutils import get_module_part, is_standard_module from pylint.interfaces import IAstroidChecker from pylint.utils import EmptyReport diff --git a/debian/control b/debian/control index 442737d..da0c1d1 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 (>= 1.1) + python-astroid (>= 1.2) Suggests: python-tk XB-Python-Version: ${python:Versions} Description: python code static checker and UML diagram generator diff --git a/lint.py b/lint.py index 26e099d..0b7b44f 100644 --- a/lint.py +++ b/lint.py @@ -38,7 +38,6 @@ from warnings import warn from logilab.common.configuration import UnsupportedAction, OptionsManagerMixIn from logilab.common.optik_ext import check_csv -from logilab.common.modutils import load_module_from_name, get_module_part from logilab.common.interface import implements from logilab.common.textutils import splitstrip, unquote from logilab.common.ureports import Table, Text, Section @@ -46,6 +45,7 @@ from logilab.common.__pkginfo__ import version as common_version from astroid import MANAGER, nodes, AstroidBuildingException from astroid.__pkginfo__ import version as astroid_version +from astroid.modutils import load_module_from_name, get_module_part from pylint.utils import ( MSG_TYPES, OPTION_RGX, diff --git a/utils.py b/utils.py index c2f1704..6fbb480 100644 --- a/utils.py +++ b/utils.py @@ -25,13 +25,13 @@ from warnings import warn from os.path import dirname, basename, splitext, exists, isdir, join, normpath from logilab.common.interface import implements -from logilab.common.modutils import modpath_from_file, get_module_files, \ - file_from_modpath, load_module_from_file from logilab.common.textutils import normalize_text from logilab.common.configuration import rest_format_section from logilab.common.ureports import Section from astroid import nodes, Module +from astroid.modutils import modpath_from_file, get_module_files, \ + file_from_modpath, load_module_from_file from pylint.interfaces import IRawChecker, ITokenChecker -- cgit v1.2.1 From afda8c7358c88969d66b3d205103b26967858713 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Wed, 23 Jul 2014 17:44:42 +0200 Subject: fix issue #203, we should not let ImportError propagate from the import checker --- ChangeLog | 4 ++++ checkers/imports.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 2d2006a..e27c966 100644 --- a/ChangeLog +++ b/ChangeLog @@ -48,6 +48,10 @@ ChangeLog for Pylint * Don't emit 'unused-variable' when assigning to a nonlocal. Closes issue #275. + + * Do not let ImportError propagate from the import checker, leading to crash + in some namespace package related cases. Closes issue #203. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the diff --git a/checkers/imports.py b/checkers/imports.py index 46ed8ac..7194134 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -299,7 +299,10 @@ 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) + try: + importedmodname = get_module_part(importedmodname) + except ImportError: + pass context_name = node.root().name if context_name == importedmodname: # module importing itself ! -- cgit v1.2.1 From bde3d6d53a4a66dcc1172a38fde3f3ae3a63b204 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Thu, 24 Jul 2014 09:40:35 +0200 Subject: go back to dependency on astroid 1.1 since it breaks tox tests, even when configured to grab astroid from hg :( --- __pkginfo__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__pkginfo__.py b/__pkginfo__.py index 2e2008b..86488fa 100644 --- a/__pkginfo__.py +++ b/__pkginfo__.py @@ -23,10 +23,10 @@ numversion = (1, 2, 1) version = '.'.join([str(num) for num in numversion]) if sys.version_info < (2, 6): - install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.2', + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1', 'StringFormat'] else: - install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.2'] + install_requires = ['logilab-common >= 0.53.0', 'astroid >= 1.1'] license = 'GPL' description = "python code static checker" -- cgit v1.2.1 From 1d6e80b96d9711634f042e5b5e9890b0998b0334 Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 24 Jul 2014 10:59:08 +0200 Subject: Restore compatibility with Python 2.6. --- checkers/strings.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/checkers/strings.py b/checkers/strings.py index ad63580..af2ceea 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -76,32 +76,38 @@ MSGS = { 'W1302': ("Invalid format string", "bad-format-string", - "Used when a PEP 3101 format string is invalid."), + "Used when a PEP 3101 format string is invalid.", + {'minversion': (2, 7)}), 'W1303': ("Missing keyword argument %r for format string", "missing-format-argument-key", "Used when a PEP 3101 format string that uses named fields " - "doesn't receive one or more required keywords."), + "doesn't receive one or more required keywords.", + {'minversion': (2, 7)}), 'W1304': ("Unused format argument %r", "unused-format-string-argument", "Used when a PEP 3101 format string that uses named " "fields is used with an argument that " - "is not required by the format string."), + "is not required by the format string.", + {'minversion': (2, 7)}), 'W1305': ("Format string contains both automatic field numbering " "and manual field specification", "format-combined-specification", "Usen when a PEP 3101 format string contains both automatic " "field numbering (e.g. '{}') and manual field " - "specification (e.g. '{0}')."), + "specification (e.g. '{0}').", + {'minversion': (2, 7)}), 'W1306': ("Missing format attribute %r in format specifier %r", "missing-format-attribute", "Used when a PEP 3101 format string uses an " "attribute specifier ({0.length}), but the argument " - "passed for formatting doesn't have that attribute."), + "passed for formatting doesn't have that attribute.", + {'minversion': (2, 7)}), 'W1307': ("Using invalid lookup key %r in format specifier %r", "invalid-format-index", "Used when a PEP 3101 format string uses a lookup specifier " "({a[1]}), but the argument passed for formatting " - "doesn't contain or doesn't have that key as an attribute.") + "doesn't contain or doesn't have that key as an attribute.", + {'minversion': (2, 7)}) } OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote, -- cgit v1.2.1 From e7cc4e247202ea5d6f5b74aab9923c2014dbede9 Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 24 Jul 2014 13:13:45 +0200 Subject: Don't emit 'pointless-string-statement' for attribute docstrings. Closes issue #193. --- ChangeLog | 3 +++ checkers/base.py | 18 +++++++++++++--- test/input/func_base_stmt_without_effect.py | 32 ++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index e27c966..b2113c4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -52,6 +52,9 @@ ChangeLog for Pylint * Do not let ImportError propagate from the import checker, leading to crash in some namespace package related cases. Closes issue #203. + * Don't emit 'pointless-string-statement' for attribute docstrings. + Closes issue #193. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the diff --git a/checkers/base.py b/checkers/base.py index 8136d0f..63b153b 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -349,7 +349,7 @@ class BasicErrorChecker(_BasicChecker): @check_messages('abstract-class-instantiated') def visit_callfunc(self, node): """ Check instantiating abstract class with - abc.ABCMeta as metaclass. + abc.ABCMeta as metaclass. """ try: infered = node.func.infer().next() @@ -373,7 +373,7 @@ class BasicErrorChecker(_BasicChecker): 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): @@ -551,6 +551,18 @@ functions, methods if isinstance(expr, astroid.Const) and isinstance(expr.value, basestring): # treat string statement in a separated message + # Handle PEP-257 attribute docstrings. + # An attribute docstring is defined as being a string right after + # an assignment at the module level, class level or __init__ level. + scope = expr.scope() + if isinstance(scope, (astroid.Class, astroid.Module, astroid.Function)): + if isinstance(scope, astroid.Function) and scope.name != '__init__': + pass + else: + sibling = expr.previous_sibling() + if (sibling.scope() is scope and + isinstance(sibling, astroid.Assign)): + return self.add_message('pointless-string-statement', node=node) return # ignore if this is : @@ -689,7 +701,7 @@ functions, methods """just print a warning on exec statements""" self.add_message('exec-used', node=node) - @check_messages('bad-builtin', 'star-args', 'eval-used', + @check_messages('bad-builtin', 'star-args', 'eval-used', 'exec-used', 'missing-reversed-argument', 'bad-reversed-sequence') def visit_callfunc(self, node): diff --git a/test/input/func_base_stmt_without_effect.py b/test/input/func_base_stmt_without_effect.py index 6454de9..4844971 100644 --- a/test/input/func_base_stmt_without_effect.py +++ b/test/input/func_base_stmt_without_effect.py @@ -11,7 +11,7 @@ 'Used when an expression that is not a function call is assigned\ to nothing. Probably something else was intended.'), """ - +# pylint: disable=too-few-public-methods __revision__ = '' __revision__ @@ -39,3 +39,33 @@ ANSWER == to_be() # W0106, typical typo to_be() or not to_be() # W0106, strange conditional function call (or nonsens) to_be().title # W0106, very strange, maybe typo +GOOD_ATTRIBUTE_DOCSTRING = 42 +"""Module level attribute docstring is fine. """ + +class ClassLevelAttributeTest(object): + """ test attribute docstrings. """ + + good_attribute_docstring = 24 + """ class level attribute docstring is fine either. """ + second_good_attribute_docstring = 42 + # Comments are good. + + # empty lines are good, too. + """ Still a valid class level attribute docstring. """ + + def __init__(self): + self.attr = 42 + """ Good attribute docstring """ + attr = 24 + """ Still a good __init__ level attribute docstring. """ + val = 0 + for val in range(42): + val += attr + """ Invalid attribute docstring """ + self.val = val + + def test(self): + """ invalid attribute docstrings here. """ + self.val = 42 + """ this is an invalid attribute docstring. """ + \ No newline at end of file -- cgit v1.2.1 From 0378d563989c10402c882bf3e8ca9af9a025a60a Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 24 Jul 2014 13:16:32 +0200 Subject: Update test message. --- test/messages/func_base_stmt_without_effect.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/messages/func_base_stmt_without_effect.txt b/test/messages/func_base_stmt_without_effect.txt index 91d9d5f..d440d02 100644 --- a/test/messages/func_base_stmt_without_effect.txt +++ b/test/messages/func_base_stmt_without_effect.txt @@ -7,4 +7,5 @@ W: 30: Expression "(list()) and (tuple())" is assigned to nothing W: 37: Expression "ANSWER == to_be()" is assigned to nothing W: 39: Expression "(to_be()) or (not to_be())" is assigned to nothing W: 40: Expression "to_be().title" is assigned to nothing - +W: 64:ClassLevelAttributeTest.__init__: String statement has no effect +W: 70:ClassLevelAttributeTest.test: String statement has no effect -- cgit v1.2.1 From 8d18e9a1d5e820e4774ee811b495f52b4071683a Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 24 Jul 2014 14:51:23 +0200 Subject: Use the proper mode for pickle when opening and writing the stats file. Closes issue #148. --- ChangeLog | 3 +++ config.py | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ChangeLog b/ChangeLog index b2113c4..9802d17 100644 --- a/ChangeLog +++ b/ChangeLog @@ -55,6 +55,9 @@ ChangeLog for Pylint * Don't emit 'pointless-string-statement' for attribute docstrings. Closes issue #193. + * Use the proper mode for pickle when opening and writing the stats file. + Closes issue #148. + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the diff --git a/config.py b/config.py index 992c293..c346a0e 100644 --- a/config.py +++ b/config.py @@ -50,15 +50,15 @@ def load_results(base): """ data_file = get_pdata_path(base, 1) try: - with open(data_file) as stream: + with open(data_file, _PICK_LOAD) as stream: return pickle.load(stream) except: return {} if sys.version_info < (3, 0): - _PICK_MOD = 'w' + _PICK_DUMP, _PICK_LOAD = 'w', 'r' else: - _PICK_MOD = 'wb' + _PICK_DUMP, _PICK_LOAD = 'wb', 'rb' def save_results(results, base): """pickle results""" @@ -69,7 +69,7 @@ def save_results(results, base): print >> sys.stderr, 'Unable to create directory %s' % PYLINT_HOME data_file = get_pdata_path(base, 1) try: - with open(data_file, _PICK_MOD) as stream: + with open(data_file, _PICK_DUMP) as stream: pickle.dump(results, stream) except (IOError, OSError), ex: print >> sys.stderr, 'Unable to create file %s: %s' % (data_file, ex) -- cgit v1.2.1 From 39e7be1a32224afa47d23e161f047feab5fbcc93 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 24 Jul 2014 16:39:07 +0200 Subject: Add a test case for #205. --- test/input/func_ctor_arguments.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/input/func_ctor_arguments.py b/test/input/func_ctor_arguments.py index 987a930..c708855 100644 --- a/test/input/func_ctor_arguments.py +++ b/test/input/func_ctor_arguments.py @@ -61,3 +61,14 @@ ClassMultiInheritance(1, 2, 3) ClassNew(1, kwarg=1) ClassNew(1, 2, 3) ClassNew(one=2) + +import abc + +def with_metaclass(meta, base=object): + """Create a new type that can be used as a metaclass.""" + return meta("NewBase", (base, ), {}) + +class ClassWithMeta(with_metaclass(abc.ABCMeta)): + pass + +ClassWithMeta() -- cgit v1.2.1 From 5878b6349b9cba56484c85442d96e58176dcac68 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 24 Jul 2014 11:36:12 +0200 Subject: Added a new test suite for functional tests. Main changes: - version restrictions are not encoded in file names any more, but in option files - expected messages are annotated in the test files directly --- MANIFEST.in | 2 +- test/functional/__init__.py | 0 test/functional/bad_inline_option.args | 2 + test/functional/bad_inline_option.py | 5 + test/functional/bad_inline_option.txt | 1 + test/functional/future_import.py | 3 + test/functional/newstyle__slots__.py | 17 ++ test/functional/newstyle__slots__.txt | 2 + test/functional/unpacked_exceptions.args | 2 + test/functional/unpacked_exceptions.py | 11 + test/functional/unpacked_exceptions.txt | 4 + test/input/func_i0010.py | 5 - test/input/func_newstyle___slots__.py | 17 -- test/input/func_noerror___future___import.py | 5 - test/input/func_unpack_exception_py_30.py | 13 -- test/messages/func_i0010.txt | 1 - test/messages/func_newstyle___slots__.txt | 2 - test/messages/func_newstyle___slots___py30.txt | 0 test/messages/func_unpack_exception_py_30.txt | 5 - test/test_func.py | 4 +- test/test_functional.py | 273 +++++++++++++++++++++++++ 21 files changed, 324 insertions(+), 50 deletions(-) create mode 100644 test/functional/__init__.py create mode 100644 test/functional/bad_inline_option.args create mode 100644 test/functional/bad_inline_option.py create mode 100644 test/functional/bad_inline_option.txt create mode 100644 test/functional/future_import.py create mode 100644 test/functional/newstyle__slots__.py create mode 100644 test/functional/newstyle__slots__.txt create mode 100644 test/functional/unpacked_exceptions.args create mode 100644 test/functional/unpacked_exceptions.py create mode 100644 test/functional/unpacked_exceptions.txt delete mode 100644 test/input/func_i0010.py delete mode 100644 test/input/func_newstyle___slots__.py delete mode 100644 test/input/func_noerror___future___import.py delete mode 100644 test/input/func_unpack_exception_py_30.py delete mode 100644 test/messages/func_i0010.txt delete mode 100644 test/messages/func_newstyle___slots__.txt delete mode 100644 test/messages/func_newstyle___slots___py30.txt delete mode 100644 test/messages/func_unpack_exception_py_30.txt create mode 100644 test/test_functional.py diff --git a/MANIFEST.in b/MANIFEST.in index 8c28721..6810ec5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,7 @@ include examples/*.py examples/pylintrc examples/pylintrc_camelcase include elisp/startup elisp/*.el include man/*.1 recursive-include doc *.rst *.jpeg Makefile *.html *.py -recursive-include test *.py *.txt *.dot *.sh +recursive-include test *.py *.txt *.dot *.sh *.args include test/input/similar* include test/input/noext include test/data/ascript diff --git a/test/functional/__init__.py b/test/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/functional/bad_inline_option.args b/test/functional/bad_inline_option.args new file mode 100644 index 0000000..b211d72 --- /dev/null +++ b/test/functional/bad_inline_option.args @@ -0,0 +1,2 @@ +[Messages Control] +enable=I diff --git a/test/functional/bad_inline_option.py b/test/functional/bad_inline_option.py new file mode 100644 index 0000000..be6e240 --- /dev/null +++ b/test/functional/bad_inline_option.py @@ -0,0 +1,5 @@ +"""errors-only is not usable as an inline option""" +# +1: [bad-inline-option] +# pylint: errors-only + +CONST = "This is not a pylint: inline option." diff --git a/test/functional/bad_inline_option.txt b/test/functional/bad_inline_option.txt new file mode 100644 index 0000000..91ac1af --- /dev/null +++ b/test/functional/bad_inline_option.txt @@ -0,0 +1 @@ +bad-inline-option:3::Unable to consider inline option 'errors-only' diff --git a/test/functional/future_import.py b/test/functional/future_import.py new file mode 100644 index 0000000..e4d3785 --- /dev/null +++ b/test/functional/future_import.py @@ -0,0 +1,3 @@ +"""a docstring""" + +from __future__ import generators diff --git a/test/functional/newstyle__slots__.py b/test/functional/newstyle__slots__.py new file mode 100644 index 0000000..306d6a1 --- /dev/null +++ b/test/functional/newstyle__slots__.py @@ -0,0 +1,17 @@ +# pylint: disable=R0903 +"""test __slots__ on old style class""" + + +class NewStyleClass(object): + """correct usage""" + __slots__ = ('a', 'b') + + +class OldStyleClass: # <3.0:[old-style-class,slots-on-old-class] + """bad usage""" + __slots__ = ('a', 'b') + + def __init__(self): + pass + +__slots__ = 'hop' diff --git a/test/functional/newstyle__slots__.txt b/test/functional/newstyle__slots__.txt new file mode 100644 index 0000000..afc9117 --- /dev/null +++ b/test/functional/newstyle__slots__.txt @@ -0,0 +1,2 @@ +old-style-class:10:OldStyleClass:Old-style class defined. +slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class diff --git a/test/functional/unpacked_exceptions.args b/test/functional/unpacked_exceptions.args new file mode 100644 index 0000000..a650233 --- /dev/null +++ b/test/functional/unpacked_exceptions.args @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.0 diff --git a/test/functional/unpacked_exceptions.py b/test/functional/unpacked_exceptions.py new file mode 100644 index 0000000..f3c633f --- /dev/null +++ b/test/functional/unpacked_exceptions.py @@ -0,0 +1,11 @@ +"""Test for redefine-in-handler, overwriting names in exception handlers.""" + +def new_style(): + """Some exceptions can be unpacked.""" + try: + pass + except IOError, (errno, message): # [unpacking-in-except] + print errno, message + # +1: [redefine-in-handler,redefine-in-handler,unpacking-in-except] + except IOError, (new_style, tuple): + print new_style, tuple diff --git a/test/functional/unpacked_exceptions.txt b/test/functional/unpacked_exceptions.txt new file mode 100644 index 0000000..ec51f20 --- /dev/null +++ b/test/functional/unpacked_exceptions.txt @@ -0,0 +1,4 @@ +unpacking-in-except:7:new_style:Implicit unpacking of exceptions is not supported in Python 3 +unpacking-in-except:10:new_style:Implicit unpacking of exceptions is not supported in Python 3 +redefine-in-handler:10:new_style:Redefining name 'new_style' from outer scope (line 3) in exception handler +redefine-in-handler:10:new_style:Redefining name 'tuple' from builtins in exception handler diff --git a/test/input/func_i0010.py b/test/input/func_i0010.py deleted file mode 100644 index d56762f..0000000 --- a/test/input/func_i0010.py +++ /dev/null @@ -1,5 +0,0 @@ -# pylint: errors-only -"""errors-only is not usable as an inline option""" -__revision__ = None - -CONST = "This is not a pylint: inline option." diff --git a/test/input/func_newstyle___slots__.py b/test/input/func_newstyle___slots__.py deleted file mode 100644 index f01c40e..0000000 --- a/test/input/func_newstyle___slots__.py +++ /dev/null @@ -1,17 +0,0 @@ -# pylint: disable=R0903 -"""test __slots__ on old style class""" - -__revision__ = 1 - -class OkOk(object): - """correct usage""" - __slots__ = ('a', 'b') - -class HaNonNonNon: - """bad usage""" - __slots__ = ('a', 'b') - - def __init__(self): - pass - -__slots__ = 'hop' # pfff diff --git a/test/input/func_noerror___future___import.py b/test/input/func_noerror___future___import.py deleted file mode 100644 index 5c77516..0000000 --- a/test/input/func_noerror___future___import.py +++ /dev/null @@ -1,5 +0,0 @@ -"""a docstring""" - -from __future__ import generators - -__revision__ = 1 diff --git a/test/input/func_unpack_exception_py_30.py b/test/input/func_unpack_exception_py_30.py deleted file mode 100644 index 356915e..0000000 --- a/test/input/func_unpack_exception_py_30.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Test for W0623, overwriting names in exception handlers.""" - -__revision__ = '' - -def new_style(): - """Some exceptions can be unpacked.""" - try: - pass - except IOError, (errno, message): # this is fine - print errno, message - except IOError, (new_style, tuple): # W0623 twice - print new_style, tuple - diff --git a/test/messages/func_i0010.txt b/test/messages/func_i0010.txt deleted file mode 100644 index 2bc4372..0000000 --- a/test/messages/func_i0010.txt +++ /dev/null @@ -1 +0,0 @@ -I: 1: Unable to consider inline option 'errors-only' diff --git a/test/messages/func_newstyle___slots__.txt b/test/messages/func_newstyle___slots__.txt deleted file mode 100644 index cb69e14..0000000 --- a/test/messages/func_newstyle___slots__.txt +++ /dev/null @@ -1,2 +0,0 @@ -C: 10:HaNonNonNon: Old-style class defined. -E: 10:HaNonNonNon: Use of __slots__ on an old style class diff --git a/test/messages/func_newstyle___slots___py30.txt b/test/messages/func_newstyle___slots___py30.txt deleted file mode 100644 index e69de29..0000000 diff --git a/test/messages/func_unpack_exception_py_30.txt b/test/messages/func_unpack_exception_py_30.txt deleted file mode 100644 index aaff92f..0000000 --- a/test/messages/func_unpack_exception_py_30.txt +++ /dev/null @@ -1,5 +0,0 @@ -W: 9:new_style: Implicit unpacking of exceptions is not supported in Python 3 -W: 11:new_style: Implicit unpacking of exceptions is not supported in Python 3 -W: 11:new_style: Redefining name 'new_style' from outer scope (line 5) in exception handler -W: 11:new_style: Redefining name 'tuple' from builtins in exception handler - diff --git a/test/test_func.py b/test/test_func.py index 63c585f..d3fb9be 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -44,6 +44,8 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" + PORTED = set(['I0001', 'I0010', 'W0712', 'E1001']) + @testlib.tag('coverage') def test_exhaustivity(self): # skip fatal messages @@ -54,7 +56,7 @@ class TestTests(testlib.TestCase): not_tested.remove(msgid) except KeyError: continue - not_tested -= set(('I0001',)) + not_tested -= self.PORTED if PY3K: not_tested.remove('W0403') # relative-import self.assertFalse(not_tested) diff --git a/test/test_functional.py b/test/test_functional.py new file mode 100644 index 0000000..ae12360 --- /dev/null +++ b/test/test_functional.py @@ -0,0 +1,273 @@ +"""Functional full-module tests for PyLint.""" +import ConfigParser +import cStringIO +import operator +import os +import re +import sys + +from logilab.common import testlib + +from pylint import lint +from pylint import reporters +from pylint import checkers + +class NoFileError(Exception): + pass + + +# If message files should be updated instead of checked. +UPDATE = False + +# Common sub-expressions. +_MESSAGE = {'msg': r'[a-z][a-z\-]+'} +# Matches a #, +# - followed by a comparison operator and a Python version (optional), +# - followed by an line number with a +/- (optional), +# - followed by a list of bracketed message symbols. +# Used to extract expected messages from testdata files. +_EXPECTED_RE = re.compile( + r'\s*#\s*(?:(?P[+-]?[0-9]+):)?' + r'(?:(?P[><=]+) *(?P[0-9.]+):)?' + r'\s*\[(?P%(msg)s(?:,\s*%(msg)s)*)\]' % _MESSAGE) + + +def parse_python_version(str): + return tuple(int(digit) for digit in str.split('.')) + + +class TestReporter(reporters.BaseReporter): + def add_message(self, msg_id, location, msg): + self.messages.append(reporters.Message(self, msg_id, location, msg)) + + def on_set_current_module(self, module, filepath): + self.messages = [] + + def display_results(self, layout): + """Ignore layouts.""" + + +class TestFile(object): + """A single functional test case file with options.""" + + _CONVERTERS = { + 'min_pyver': parse_python_version, + 'max_pyver': parse_python_version, + } + + + def __init__(self, directory, filename): + self._directory = directory + self.base = filename.replace('.py', '') + self.options = { + 'min_pyver': (2, 5), + 'max_pyver': (4, 0), + } + self._parse_options() + + def _parse_options(self): + cp = ConfigParser.ConfigParser() + cp.add_section('testoptions') + try: + cp.read(self.option_file) + except NoFileError: + pass + + for name, value in cp.items('testoptions'): + conv = self._CONVERTERS.get(name, lambda v: v) + self.options[name] = conv(value) + + @property + def option_file(self): + return self._file_type('.args') + + @property + def module(self): + package = os.path.basename(self._directory) + return '.'.join([package, self.base]) + + @property + def expected_output(self): + return self._file_type('.txt') + + @property + def source(self): + return self._file_type('.py') + + def _file_type(self, ext): + name = os.path.join(self._directory, self.base + ext) + if os.path.exists(name): + return name + else: + raise NoFileError + + +_OPERATORS = { + '>': operator.gt, + '<': operator.lt, + '>=': operator.ge, + '<=': operator.le, +} + +def get_expected_messages(stream): + """Parses a file and get expected messages. + + :param stream: File-like input stream. + :returns: A dict mapping line,msg-symbol tuples to the count on this line. + """ + messages = {} + for i, line in enumerate(stream): + match = _EXPECTED_RE.search(line) + if match is None: + continue + line = match.group('line') + if line is None: + line = i + 1 + elif line.startswith('+') or line.startswith('-'): + line = i + 1 + int(line) + else: + line = int(line) + + version = match.group('version') + op = match.group('op') + if version: + required = parse_python_version(version) + if not _OPERATORS[op](sys.version_info, required): + continue + + for msg_id in match.group('msgs').split(','): + messages.setdefault((line, msg_id.strip()), 0) + messages[line, msg_id.strip()] += 1 + return messages + + +def multiset_difference(left_op, right_op): + """Takes two multisets and compares them. + + A multiset is a dict with the cardinality of the key as the value. + + :param left_op: The expected entries. + :param right_op: Actual entries. + + :returns: The two multisets of missing and unexpected messages. + """ + missing = left_op.copy() + unexpected = {} + for key, value in right_op.iteritems(): + missing.setdefault(key, 0) + missing[key] -= value + if missing[key] == 0: + del missing[key] + elif missing[key] < 0: + unexpected.setdefault(key, 0) + unexpected[key] = -missing.pop(key) + return missing, unexpected + + +class LintModuleTest(testlib.TestCase): + def __init__(self, test_file): + super(LintModuleTest, self).__init__() + test_reporter = TestReporter() + self._linter = lint.PyLinter() + self._linter.set_reporter(test_reporter) + self._linter.config.persistent = 0 + self._linter.disable('I') + try: + self._linter.load_file_configuration(test_file.option_file) + except NoFileError: + pass + checkers.initialize(self._linter) + self._test_file = test_file + + def shortDescription(self): + return self._test_file.base + + def _produces_output(self): + return True + + def _get_expected(self): + with open(self._test_file.source) as fobj: + expected = get_expected_messages(fobj) + + lines = [] + if self._produces_output() and expected: + with open(self._test_file.expected_output, 'U') as fobj: + for line in fobj: + parts = line.split(':') + linenum = int(parts[1]) + if (linenum, parts[0]) in expected: + lines.append(line) + return expected, ''.join(lines) + + def _get_received(self): + messages = self._linter.reporter.messages + messages.sort(key=lambda m: (m.line, m.C, m.msg)) + text_result = cStringIO.StringIO() + received = {} + for msg in messages: + received.setdefault((msg.line, msg.symbol), 0) + received[msg.line, msg.symbol] += 1 + text_result.write(msg.format('{symbol}:{line}:{obj}:{msg}')) + text_result.write('\n') + return received, text_result.getvalue() + + def runTest(self): + self._linter.check([self._test_file.module]) + + expected_messages, expected_text = self._get_expected() + received_messages, received_text = self._get_received() + + if expected_messages != received_messages: + msg = ['Wrong results for file "%s":' % (self._test_file.base)] + missing, unexpected = multiset_difference(expected_messages, + received_messages) + if missing: + msg.append('\nExpected in testdata:') + msg.extend(' %3d: %s' % msg for msg in sorted(missing)) + if unexpected: + msg.append('\nUnexpected in testdata:') + msg.extend(' %3d: %s' % msg for msg in sorted(unexpected)) + self.fail('\n'.join(msg)) + self._check_output_text(expected_messages, expected_text, received_text) + + def _check_output_text(self, expected_messages, expected_text, received_text): + self.assertMultiLineEqual(expected_text, received_text) + + +class LintModuleOutputUpdate(LintModuleTest): + def _produces_output(self): + return False + + def _check_output_text(self, expected_messages, expected_text, received_text): + if expected_messages: + with open(self._test_file.expected_output, 'w') as fobj: + fobj.write(received_text) + + +def active_in_running_python_version(options): + return options['min_pyver'] < sys.version_info <= options['max_pyver'] + + +def suite(): + input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'functional') + suite = testlib.TestSuite() + for fname in os.listdir(input_dir): + if fname != '__init__.py' and fname.endswith('.py'): + test_file = TestFile(input_dir, fname) + if active_in_running_python_version(test_file.options): + if UPDATE: + suite.addTest(LintModuleOutputUpdate(test_file)) + else: + suite.addTest(LintModuleTest(test_file)) + return suite + + +# TODO(tmarek): Port exhaustivity test from test_func once all tests have been added. + + +if __name__=='__main__': + if '-u' in sys.argv: + UPDATE = True + sys.argv.remove('-u') + testlib.unittest_main(defaultTest='suite') -- cgit v1.2.1 From 661eb896e31f6ac4c1ff26858daaf3ee59d7013c Mon Sep 17 00:00:00 2001 From: Marco Forte Date: Thu, 14 Aug 2014 14:13:46 +0000 Subject: tiny typo fix --- doc/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/installation.rst b/doc/installation.rst index 56115c7..10862a8 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -62,7 +62,7 @@ On Windows, once you have installed Pylint, the command line usage is :: pylint.bat [options] module_or_package But this will only work if *pylint.bat* is either in the current -directory, or on your system path. (*setup.py* install install *python.bat* +directory, or on your system path. (*setup.py* will install *python.bat* to the *Scripts* subdirectory of your Python installation -- e.g. C:\Python24\Scripts.) You can do any of the following to solve this: -- cgit v1.2.1 -- cgit v1.2.1 From 2619e069558471d6600cf691f2673e520c087668 Mon Sep 17 00:00:00 2001 From: cpopa Date: Thu, 14 Aug 2014 18:20:13 +0300 Subject: Don't emit 'protected-access' if the attribute is accessed using a property defined at the class level. --- ChangeLog | 3 +++ checkers/classes.py | 19 ++++++++++++++++++- .../func_noerror_classes_protected_member_access.py | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 7b70fa7..f7c3b90 100644 --- a/ChangeLog +++ b/ChangeLog @@ -63,6 +63,9 @@ ChangeLog for Pylint * Look in the metaclass, if defined, for members not found in the current class. Closes issue #306. + * Don't emit 'protected-access' if the attribute is accessed using + a property defined at the class level. + 2014-07-26 -- 1.3.0 * Allow hanging continued indentation for implicitly concatenated diff --git a/checkers/classes.py b/checkers/classes.py index 90b2fbe..ebca3f2 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -27,7 +27,7 @@ from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker from pylint.checkers.utils import ( PYMETHODS, overrides_a_method, check_messages, is_attr_private, - is_attr_protected, node_frame_class, safe_infer) + is_attr_protected, node_frame_class, safe_infer, is_builtin_object) if sys.version_info >= (3, 0): NEXT_METHOD = '__next__' @@ -581,6 +581,23 @@ a metaclass class method.'} # We are in a class, one remaining valid cases, Klass._attr inside # Klass if not (callee == klass.name or callee in klass.basenames): + # Detect property assignments in the body of the class. + # This is acceptable: + # + # class A: + # b = property(lambda: self._b) + + stmt = node.parent.statement() + try: + if (isinstance(stmt, astroid.Assign) and + (stmt in klass.body or klass.parent_of(stmt)) and + isinstance(stmt.value, astroid.CallFunc) and + isinstance(stmt.value.func, astroid.Name) and + stmt.value.func.name == 'property' and + is_builtin_object(next(stmt.value.func.infer(), None))): + return + except astroid.InferenceError: + pass self.add_message('protected-access', node=node, args=attrname) def visit_name(self, node): diff --git a/test/input/func_noerror_classes_protected_member_access.py b/test/input/func_noerror_classes_protected_member_access.py index bef1bff..eeff97d 100644 --- a/test/input/func_noerror_classes_protected_member_access.py +++ b/test/input/func_noerror_classes_protected_member_access.py @@ -22,3 +22,4 @@ class A3123(object): smeth = staticmethod(smeth) + prop = property(lambda self: self._protected) -- cgit v1.2.1 From 91af7c933718f518c819fd9eb9f3aa3dd967f43b Mon Sep 17 00:00:00 2001 From: cpopa Date: Sat, 16 Aug 2014 00:25:57 +0300 Subject: Detect calls of the parent's __init__, through a binded super() call. --- ChangeLog | 3 +++ checkers/classes.py | 11 +++++++++++ test/input/func_w0233.py | 14 ++++++++++++++ test/messages/func_w0233.txt | 1 + 4 files changed, 29 insertions(+) diff --git a/ChangeLog b/ChangeLog index f7c3b90..c6b9b03 100644 --- a/ChangeLog +++ b/ChangeLog @@ -66,6 +66,9 @@ ChangeLog for Pylint * Don't emit 'protected-access' if the attribute is accessed using a property defined at the class level. + * Detect calls of the parent's __init__, through a binded super() call. + + 2014-07-26 -- 1.3.0 * Allow hanging continued indentation for implicitly concatenated diff --git a/checkers/classes.py b/checkers/classes.py index ebca3f2..d9ebd8d 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -818,6 +818,17 @@ a metaclass class method.'} klass = expr.expr.infer().next() if klass is YES: continue + # The infered klass can be super(), which was + # assigned to a variable and the `__init__` was called later. + # + # base = super() + # base.__init__(...) + + if (isinstance(klass, astroid.Instance) and + isinstance(klass._proxied, astroid.Class) and + is_builtin_object(klass._proxied) and + klass._proxied.name == 'super'): + return try: del not_called_yet[klass] except KeyError: diff --git a/test/input/func_w0233.py b/test/input/func_w0233.py index 857743a..ef5f205 100644 --- a/test/input/func_w0233.py +++ b/test/input/func_w0233.py @@ -34,3 +34,17 @@ class DDDD(AAAA): AAAA.__init__(self) else: AAAA.__init__(self) + +class Super(dict): + """ test late binding super() call """ + def __init__(self): + base = super() + base.__init__() + +class Super2(dict): + """ Using the same idiom as Super, but without calling + the __init__ method. + """ + def __init__(self): + base = super() + base.__woohoo__() diff --git a/test/messages/func_w0233.txt b/test/messages/func_w0233.txt index 614ceac..157b270 100644 --- a/test/messages/func_w0233.txt +++ b/test/messages/func_w0233.txt @@ -2,3 +2,4 @@ E: 22:CCC: Module 'input.func_w0233' has no 'BBBB' member E: 27:CCC.__init__: Module 'input.func_w0233' has no 'BBBB' member F: 20: Unable to import 'nonexistant' W: 12:AAAA.__init__: __init__ method from a non direct base class 'BBBBMixin' is called +W: 48:Super2.__init__: __init__ method from base class 'dict' is not called \ No newline at end of file -- cgit v1.2.1 From 7c50e98ba61dc650e995eb0c106ba2487e6e359f Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 17 Aug 2014 22:35:28 +0300 Subject: Check that a class has an explicitly defined metaclass before emitting 'old-style-class' for Python 2. This can catch metaclasses defined at module level, using `__metaclass__ = ...` syntax, which transforms all old style classes to newstyle. Also, look for the .newstyle attribute instead of bases. --- ChangeLog | 3 +++ checkers/newstyle.py | 10 +++++----- test/functional/old_style_class_py27.py | 17 +++++++++++++++++ test/functional/old_style_class_py27.rc | 2 ++ test/functional/old_style_class_py27.txt | 2 ++ test/functional/undefined_variable.py | 2 +- 6 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 test/functional/old_style_class_py27.py create mode 100644 test/functional/old_style_class_py27.rc create mode 100644 test/functional/old_style_class_py27.txt diff --git a/ChangeLog b/ChangeLog index c6b9b03..dc85ee4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -68,6 +68,9 @@ ChangeLog for Pylint * Detect calls of the parent's __init__, through a binded super() call. + * Check that a class has an explicitly defined metaclass before + emitting 'old-style-class' for Python 2. + 2014-07-26 -- 1.3.0 diff --git a/checkers/newstyle.py b/checkers/newstyle.py index cc8f640..532f3f1 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -74,14 +74,14 @@ class NewStyleConflictChecker(BaseChecker): @check_messages('slots-on-old-class', 'old-style-class') def visit_class(self, node): - """check __slots__ usage + """ Check __slots__ in old style classes and old + style class definition. """ if '__slots__' in node and not node.newstyle: self.add_message('slots-on-old-class', node=node) - # The node type could be class, exception, metaclass, or - # interface. Presumably, the non-class-type nodes would always - # have an explicit base class anyway. - if not node.bases and node.type == 'class': + # If the class node is not marked as newstyle and it has no explicitly + # defined metaclass, then it's an old style class. + if not node.newstyle and node.type == 'class' and not node.metaclass(): self.add_message('old-style-class', node=node) @check_messages('property-on-old-class') diff --git a/test/functional/old_style_class_py27.py b/test/functional/old_style_class_py27.py new file mode 100644 index 0000000..4073c73 --- /dev/null +++ b/test/functional/old_style_class_py27.py @@ -0,0 +1,17 @@ +""" Tests for old style classes. """ +# pylint: disable=no-init, too-few-public-methods, invalid-name + +class Old: # [old-style-class] + """ old style class """ + +class Child(Old): # [old-style-class] + """ still an old style class """ + +__metaclass__ = type + +class NotOldStyle: + """ Because I have a metaclass at global level. """ + +class NotOldStyle2: + """ Because I have a metaclass at class level. """ + __metaclass__ = type diff --git a/test/functional/old_style_class_py27.rc b/test/functional/old_style_class_py27.rc new file mode 100644 index 0000000..a650233 --- /dev/null +++ b/test/functional/old_style_class_py27.rc @@ -0,0 +1,2 @@ +[testoptions] +max_pyver=3.0 diff --git a/test/functional/old_style_class_py27.txt b/test/functional/old_style_class_py27.txt new file mode 100644 index 0000000..223233e --- /dev/null +++ b/test/functional/old_style_class_py27.txt @@ -0,0 +1,2 @@ +old-style-class:4::Old-style class defined. +old-style-class:7::Old-style class defined. diff --git a/test/functional/undefined_variable.py b/test/functional/undefined_variable.py index 25f0145..003708c 100644 --- a/test/functional/undefined_variable.py +++ b/test/functional/undefined_variable.py @@ -1,5 +1,5 @@ """Test warnings about access to undefined variables.""" -# pylint: disable=too-few-public-methods, no-init, no-self-use +# pylint: disable=too-few-public-methods, no-init, no-self-use, old-style-class DEFINED = 1 -- cgit v1.2.1 From 98d800bdab4f9d7209b0466ee62036638c511abd Mon Sep 17 00:00:00 2001 From: cpopa Date: Sun, 17 Aug 2014 22:42:13 +0300 Subject: Fix test failures. --- test/functional/old_style_class_py27.txt | 4 ++-- test/input/func_noerror_access_attr_before_def_false_positive.py | 2 +- test/input/func_noerror_indirect_interface.py | 2 +- test/messages/func_w0231.txt | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/functional/old_style_class_py27.txt b/test/functional/old_style_class_py27.txt index 223233e..c46a7b6 100644 --- a/test/functional/old_style_class_py27.txt +++ b/test/functional/old_style_class_py27.txt @@ -1,2 +1,2 @@ -old-style-class:4::Old-style class defined. -old-style-class:7::Old-style class defined. +old-style-class:4:Old:Old-style class defined. +old-style-class:7:Child:Old-style class defined. diff --git a/test/input/func_noerror_access_attr_before_def_false_positive.py b/test/input/func_noerror_access_attr_before_def_false_positive.py index 3bf966b..7f8bbb4 100644 --- a/test/input/func_noerror_access_attr_before_def_false_positive.py +++ b/test/input/func_noerror_access_attr_before_def_false_positive.py @@ -1,4 +1,4 @@ -#pylint: disable=C0103,R0904,R0903,W0201 +#pylint: disable=C0103,R0904,R0903,W0201,old-style-class """ This module demonstrates a possible problem of pyLint with calling __init__ s from inherited classes. diff --git a/test/input/func_noerror_indirect_interface.py b/test/input/func_noerror_indirect_interface.py index a0f245a..56d9ddd 100644 --- a/test/input/func_noerror_indirect_interface.py +++ b/test/input/func_noerror_indirect_interface.py @@ -1,7 +1,7 @@ """shows a bug where pylint can't find interfaces when they are used indirectly. See input/indirect[123].py for details on the setup""" - +# pylint: disable=old-style-class __revision__ = None from input.indirect2 import AbstractToto diff --git a/test/messages/func_w0231.txt b/test/messages/func_w0231.txt index 8cbf0b7..2f97fba 100644 --- a/test/messages/func_w0231.txt +++ b/test/messages/func_w0231.txt @@ -1,6 +1,7 @@ C: 7:AAAA: Old-style class defined. C: 13:BBBB: Old-style class defined. C: 19:CCCC: Old-style class defined. +C: 23:ZZZZ: Old-style class defined. W: 19:CCCC: Class has no __init__ method W: 26:ZZZZ.__init__: __init__ method from base class 'BBBB' is not called W: 59:AssignedInit.__init__: __init__ method from base class 'NewStyleC' is not called -- cgit v1.2.1 From 19ea91dd27f970d9bd30cf9551b2770ab0fbb16b Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 10:55:46 +0300 Subject: Revert change from 3b748b4: emit old-style-classes only for classes without bases and without explicitly defined metaclasses. --- checkers/newstyle.py | 7 ++++--- test/functional/old_style_class_py27.py | 12 ++++++------ test/messages/func_w0231.txt | 1 - 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/checkers/newstyle.py b/checkers/newstyle.py index 532f3f1..cf50229 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -79,9 +79,10 @@ class NewStyleConflictChecker(BaseChecker): """ if '__slots__' in node and not node.newstyle: self.add_message('slots-on-old-class', node=node) - # If the class node is not marked as newstyle and it has no explicitly - # defined metaclass, then it's an old style class. - if not node.newstyle and node.type == 'class' and not node.metaclass(): + # The node type could be class, exception, metaclass, or + # interface. Presumably, the non-class-type nodes would always + # have an explicit base class anyway. + if not node.bases and node.type == 'class' and not node.metaclass(): self.add_message('old-style-class', node=node) @check_messages('property-on-old-class') diff --git a/test/functional/old_style_class_py27.py b/test/functional/old_style_class_py27.py index 4073c73..2c3c126 100644 --- a/test/functional/old_style_class_py27.py +++ b/test/functional/old_style_class_py27.py @@ -4,14 +4,14 @@ class Old: # [old-style-class] """ old style class """ -class Child(Old): # [old-style-class] - """ still an old style class """ +class Child(Old): + """ Old style class, but don't emit for it. """ + +class NotOldStyle2: + """ Because I have a metaclass at class level. """ + __metaclass__ = type __metaclass__ = type class NotOldStyle: """ Because I have a metaclass at global level. """ - -class NotOldStyle2: - """ Because I have a metaclass at class level. """ - __metaclass__ = type diff --git a/test/messages/func_w0231.txt b/test/messages/func_w0231.txt index 2f97fba..8cbf0b7 100644 --- a/test/messages/func_w0231.txt +++ b/test/messages/func_w0231.txt @@ -1,7 +1,6 @@ C: 7:AAAA: Old-style class defined. C: 13:BBBB: Old-style class defined. C: 19:CCCC: Old-style class defined. -C: 23:ZZZZ: Old-style class defined. W: 19:CCCC: Class has no __init__ method W: 26:ZZZZ.__init__: __init__ method from base class 'BBBB' is not called W: 59:AssignedInit.__init__: __init__ method from base class 'NewStyleC' is not called -- cgit v1.2.1 From f72c2e1a0e0943594740475c1e6071a111562eeb Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 14:34:23 +0300 Subject: Emit 'catching-non-exception' for non-class nodes. Closes issue #303. --- ChangeLog | 2 + checkers/exceptions.py | 63 ++++++++++++++++++++++++--- test/functional/invalid_exceptions_caught.py | 18 ++++++++ test/functional/invalid_exceptions_caught.txt | 3 ++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index dc85ee4..cd25ca8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -71,6 +71,8 @@ ChangeLog for Pylint * Check that a class has an explicitly defined metaclass before emitting 'old-style-class' for Python 2. + * Emit 'catching-non-exception' for non-class nodes. Closes issue #303. + 2014-07-26 -- 1.3.0 diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 81520ce..ecbf4a4 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -19,7 +19,7 @@ import sys from logilab.common.compat import builtins BUILTINS_NAME = builtins.__name__ import astroid -from astroid import YES, Instance, unpack_infer +from astroid import YES, Instance, unpack_infer, List, Tuple from pylint.checkers import BaseChecker from pylint.checkers.utils import ( @@ -28,6 +28,36 @@ from pylint.checkers.utils import ( EXCEPTIONS_MODULE) from pylint.interfaces import IAstroidChecker +def _annotated_unpack_infer(stmt, context=None): + """ + Recursively generate nodes inferred by the given statement. + If the inferred value is a list or a tuple, recurse on the elements. + Returns an iterator which yields tuples in the format + ('original node', 'infered node'). + """ + # TODO: the same code as unpack_infer, except for the annotated + # return. We need this type of annotation only here and + # there is no point in complicating the API for unpack_infer. + # If the need arises, this behaviour can be promoted to unpack_infer + # as well. + if isinstance(stmt, (List, Tuple)): + for elt in stmt.elts: + for infered_elt in unpack_infer(elt, context): + yield elt, infered_elt + return + # if infered is a final node, return it and stop + infered = next(stmt.infer(context)) + if infered is stmt: + yield stmt, infered + return + # else, infer recursivly, except YES object that should be returned as is + for infered in stmt.infer(context): + if infered is YES: + yield stmt, infered + else: + for inf_inf in unpack_infer(infered, context): + yield stmt, inf_inf + def infer_bases(klass): """ Fully infer the bases of the klass node. @@ -255,13 +285,34 @@ class ExceptionsChecker(BaseChecker): node=handler, args=handler.type.op) else: try: - excs = list(unpack_infer(handler.type)) + excs = list(_annotated_unpack_infer(handler.type)) except astroid.InferenceError: continue - for exc in excs: - # XXX skip other non class nodes - if exc is YES or not isinstance(exc, astroid.Class): + for part, exc in excs: + if exc is YES: continue + if not isinstance(exc, astroid.Class): + # Don't emit the warning if the infered stmt + # is None, but the exception handler is something else, + # maybe it was redefined. + if (isinstance(exc, astroid.Const) and + exc.value is None): + if ((isinstance(handler.type, astroid.Const) and + handler.type.value is None) or + handler.type.parent_of(exc)): + # If the exception handler catches None or + # the exception component, which is None, is + # defined by the entire exception handler, then + # emit a warning. + self.add_message('catching-non-exception', + node=handler.type, + args=(part.as_string(), )) + else: + self.add_message('catching-non-exception', + node=handler.type, + args=(part.as_string(), )) + continue + exc_ancestors = [anc for anc in exc.ancestors() if isinstance(anc, astroid.Class)] for previous_exc in exceptions_classes: @@ -289,7 +340,7 @@ class ExceptionsChecker(BaseChecker): node=handler.type, args=(exc.name, )) - exceptions_classes += excs + exceptions_classes += [exc for _, exc in excs] def register(linter): diff --git a/test/functional/invalid_exceptions_caught.py b/test/functional/invalid_exceptions_caught.py index 99872d2..0bd0f7b 100644 --- a/test/functional/invalid_exceptions_caught.py +++ b/test/functional/invalid_exceptions_caught.py @@ -45,3 +45,21 @@ try: 1 + 3 except (SkipException, SecondSkipException): print "caught" + +try: + 1 + 42 +# +1:[catching-non-exception,catching-non-exception] +except (None, list()): + print "caught" + +try: + 1 + 24 +except None: # [catching-non-exception] + print "caught" + +EXCEPTION = None +EXCEPTION = ZeroDivisionError +try: + 1 + 46 +except EXCEPTION: + print "caught" diff --git a/test/functional/invalid_exceptions_caught.txt b/test/functional/invalid_exceptions_caught.txt index 530ef9a..55b653b 100644 --- a/test/functional/invalid_exceptions_caught.txt +++ b/test/functional/invalid_exceptions_caught.txt @@ -1,3 +1,6 @@ catching-non-exception:25::Catching an exception which doesn't inherit from BaseException: MyException catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MyException catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MySecondException +catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: None +catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: list() +catching-non-exception:57::Catching an exception which doesn't inherit from BaseException: None -- cgit v1.2.1 From 41a05fd331428b7805a806d37bccbc69a887910f Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 14:52:14 +0300 Subject: Add debug code for drone.io. --- testutils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testutils.py b/testutils.py index ef21211..e53ec70 100644 --- a/testutils.py +++ b/testutils.py @@ -245,6 +245,7 @@ class LintTestUsingModule(unittest.TestCase): depends = None output = None _TEST_TYPE = 'module' + maxDiff = None def shortDescription(self): values = {'mode' : self._TEST_TYPE, -- cgit v1.2.1 From 762ba61a3e8329ce5036f6a3dbde8173d0902846 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:02:14 +0300 Subject: Add debug code for drone.io. --- checkers/exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index ecbf4a4..7af0aed 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -336,6 +336,7 @@ class ExceptionsChecker(BaseChecker): fully_infered = all(inferit is not YES for inferit in bases) if fully_infered: + print("Infered", fully_infered, bases) self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) -- cgit v1.2.1 From 61b31662f6119e9031557f42837b8288b293541c Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:04:42 +0300 Subject: Uh, drone.io. --- checkers/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 7af0aed..8c9bea9 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -336,7 +336,7 @@ class ExceptionsChecker(BaseChecker): fully_infered = all(inferit is not YES for inferit in bases) if fully_infered: - print("Infered", fully_infered, bases) + print("Infered", fully_infered, bases, exc) self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) -- cgit v1.2.1 From ce4ba79604eef9316106e29adf4ca425450d7263 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:06:38 +0300 Subject: Debug. --- checkers/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 8c9bea9..3dbf81f 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -295,6 +295,7 @@ class ExceptionsChecker(BaseChecker): # Don't emit the warning if the infered stmt # is None, but the exception handler is something else, # maybe it was redefined. + print("not a class", exc, type(exc), exc.as_string()) if (isinstance(exc, astroid.Const) and exc.value is None): if ((isinstance(handler.type, astroid.Const) and @@ -336,7 +337,6 @@ class ExceptionsChecker(BaseChecker): fully_infered = all(inferit is not YES for inferit in bases) if fully_infered: - print("Infered", fully_infered, bases, exc) self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) -- cgit v1.2.1 From 67752720f07f1d62dfc8ed38e3d2e2a96ebccfa3 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:09:35 +0300 Subject: Backed out changeset ff26eefc7e0c --- checkers/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 3dbf81f..8c9bea9 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -295,7 +295,6 @@ class ExceptionsChecker(BaseChecker): # Don't emit the warning if the infered stmt # is None, but the exception handler is something else, # maybe it was redefined. - print("not a class", exc, type(exc), exc.as_string()) if (isinstance(exc, astroid.Const) and exc.value is None): if ((isinstance(handler.type, astroid.Const) and @@ -337,6 +336,7 @@ class ExceptionsChecker(BaseChecker): fully_infered = all(inferit is not YES for inferit in bases) if fully_infered: + print("Infered", fully_infered, bases, exc) self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) -- cgit v1.2.1 From 5e4d1597d8260de4836e2652b424310e74690c4c Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:09:57 +0300 Subject: Backout changeset. --- checkers/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 8c9bea9..7af0aed 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -336,7 +336,7 @@ class ExceptionsChecker(BaseChecker): fully_infered = all(inferit is not YES for inferit in bases) if fully_infered: - print("Infered", fully_infered, bases, exc) + print("Infered", fully_infered, bases) self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) -- cgit v1.2.1 From 766333545bc67f42aa4710fd08bebba5f55eb7e4 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:10:27 +0300 Subject: Backout changeset. --- checkers/exceptions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 7af0aed..ecbf4a4 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -336,7 +336,6 @@ class ExceptionsChecker(BaseChecker): fully_infered = all(inferit is not YES for inferit in bases) if fully_infered: - print("Infered", fully_infered, bases) self.add_message('catching-non-exception', node=handler.type, args=(exc.name, )) -- cgit v1.2.1 From fc3d7edd86a6ca68791fa53c08a94e39406bf7f8 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:13:34 +0300 Subject: Unproxy the infered instances. --- checkers/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index ecbf4a4..0142d9a 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -291,6 +291,8 @@ class ExceptionsChecker(BaseChecker): for part, exc in excs: if exc is YES: continue + if isinstance(exc, astroid.Instance): + exc = exc._proxied if not isinstance(exc, astroid.Class): # Don't emit the warning if the infered stmt # is None, but the exception handler is something else, -- cgit v1.2.1 From 93e71976377c537fc87a4ca1fbcedd4e6f597096 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 15:23:11 +0300 Subject: Check if the instance inherits from a standard exception before unproxying. --- checkers/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index 0142d9a..cd78719 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -291,7 +291,7 @@ class ExceptionsChecker(BaseChecker): for part, exc in excs: if exc is YES: continue - if isinstance(exc, astroid.Instance): + if isinstance(exc, astroid.Instance) and inherit_from_std_ex(exc): exc = exc._proxied if not isinstance(exc, astroid.Class): # Don't emit the warning if the infered stmt -- cgit v1.2.1 From 168db0ec2772579d2dae24f1971a84546f8675f3 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 17:54:07 +0300 Subject: Order of reporting is consistent. --- ChangeLog | 2 ++ utils.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index cd25ca8..3572212 100644 --- a/ChangeLog +++ b/ChangeLog @@ -73,6 +73,8 @@ ChangeLog for Pylint * Emit 'catching-non-exception' for non-class nodes. Closes issue #303. + * Order of reporting is consistent. + 2014-07-26 -- 1.3.0 diff --git a/utils.py b/utils.py index b476dc1..deef486 100644 --- a/utils.py +++ b/utils.py @@ -640,6 +640,21 @@ class ReportsHandlerMixIn(object): self._reports = {} self._reports_state = {} + @property + def _sorted_reports(self): + """ Return a list of reports, sorted in the order + of their reporting priority. + """ + reports = sorted(self._reports, key=lambda x: getattr(x, 'name', '')) + try: + # Remove the current reporter and add it + # at the end of the list. + reports.pop(reports.index(self)) + except ValueError: + pass + reports.append(self) + return reports + def register_report(self, reportid, r_title, r_cb, checker): """register a report @@ -671,7 +686,7 @@ class ReportsHandlerMixIn(object): """render registered reports""" sect = Section('Report', '%s statements analysed.'% (self.stats['statement'])) - for checker in self._reports: + for checker in self._sorted_reports: for reportid, r_title, r_cb in self._reports[checker]: if not self.report_is_enabled(reportid): continue -- cgit v1.2.1 From 54e8ee1d68f66b5a9f89d48209c24494a56d68d9 Mon Sep 17 00:00:00 2001 From: cpopa Date: Mon, 18 Aug 2014 17:57:37 +0300 Subject: Add the current reporter only if it was removed. --- utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils.py b/utils.py index deef486..9a7189e 100644 --- a/utils.py +++ b/utils.py @@ -652,7 +652,8 @@ class ReportsHandlerMixIn(object): reports.pop(reports.index(self)) except ValueError: pass - reports.append(self) + else: + reports.append(self) return reports def register_report(self, reportid, r_title, r_cb, checker): -- cgit v1.2.1 From faae5267f9541ffbce013f3505e26295698a43b9 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 24 Jul 2014 14:13:09 +0200 Subject: Change the multi-style name checker from first-style-wins to majority-style-wins. --- checkers/base.py | 44 +++++++++++++++++++++++++++++++++---------- doc/options.rst | 13 +++++++------ test/unittest_checker_base.py | 12 ++++++++---- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/checkers/base.py b/checkers/base.py index 6e5804f..d6553ed 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -16,6 +16,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """basic checker for Python code""" +import collections +import itertools import sys import astroid from logilab.common.ureports import Table @@ -944,6 +946,7 @@ class NameChecker(_BasicChecker): _BasicChecker.__init__(self, linter) self._name_category = {} self._name_group = {} + self._bad_names = {} def open(self): self.stats = self.linter.add_stats(badname_module=0, @@ -961,6 +964,25 @@ class NameChecker(_BasicChecker): @check_messages('blacklisted-name', 'invalid-name') def visit_module(self, node): self._check_name('module', node.name.split('.')[-1], node) + self._bad_names = {} + + def leave_module(self, node): + for category, all_groups in self._bad_names.iteritems(): + if len(all_groups) < 2: + continue + groups = collections.defaultdict(list) + min_warnings = sys.maxint + for group in all_groups.itervalues(): + groups[len(group)].append(group) + min_warnings = min(len(group), min_warnings) + if len(groups[min_warnings]) > 1: + by_line = sorted(groups[min_warnings], + key=lambda group: min(warning[0].lineno for warning in group)) + warnings = itertools.chain(*by_line[1:]) + else: + warnings = groups[min_warnings][0] + for args in warnings: + self._raise_name_warning(*args) @check_messages('blacklisted-name', 'invalid-name') def visit_class(self, node): @@ -1030,6 +1052,14 @@ class NameChecker(_BasicChecker): match.lastgroup is not None and match.lastgroup not in EXEMPT_NAME_CATEGORIES) + def _raise_name_warning(self, node, node_type, name): + 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 + def _check_name(self, node_type, name, node): """check for a name using the type's regexp""" if is_inside_except(node): @@ -1047,18 +1077,12 @@ class NameChecker(_BasicChecker): 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 + bad_name_group = self._bad_names.setdefault(name_group, {}) + warnings = bad_name_group.setdefault(match.lastgroup, []) + warnings.append((node, node_type, name)) 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 + self._raise_name_warning(node, node_type, name) class DocStringChecker(_BasicChecker): diff --git a/doc/options.rst b/doc/options.rst index 4a159f2..702d93c 100644 --- a/doc/options.rst +++ b/doc/options.rst @@ -91,8 +91,8 @@ 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. +Rather than emitting name warnings immediately, PyLint will determine the +prevalent naming style inside each module and enforce it on all names. Consider the following (simplified) example:: @@ -101,16 +101,17 @@ Consider the following (simplified) example:: 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:: +In ``sample.py``, the function name on line 1 and 7 will mark the module +and enforce the match of named group ``snake`` for the remaining names in +the module:: - def trigger_snake_case(arg): + def valid_snake_case(arg): ... def InvalidCamelCase(arg): ... - def valid_snake_case(arg): + def more_valid_snake_case(arg): ... Because of this, the name on line 4 will trigger an ``invalid-name`` warning, diff --git a/test/unittest_checker_base.py b/test/unittest_checker_base.py index fe0dbca..4f16a77 100644 --- a/test/unittest_checker_base.py +++ b/test/unittest_checker_base.py @@ -161,18 +161,19 @@ class MultiNamingStyleTest(CheckerTestCase): MULTI_STYLE_RE = re.compile('(?:(?P[A-Z]+)|(?P[a-z]+))$') @set_config(class_rgx=MULTI_STYLE_RE) - def test_multi_name_detection_first(self): + def test_multi_name_detection_majority(self): classes = test_utils.extract_node(""" - class CLASSA(object): #@ - pass class classb(object): #@ pass + class CLASSA(object): #@ + pass class CLASSC(object): #@ pass """) - with self.assertAddsMessages(Message('invalid-name', node=classes[1], args=('class', 'classb', ''))): + with self.assertAddsMessages(Message('invalid-name', node=classes[0], args=('class', 'classb', ''))): for cls in classes: self.checker.visit_class(cls) + self.checker.leave_module(cls.root) @set_config(class_rgx=MULTI_STYLE_RE) def test_multi_name_detection_first_invalid(self): @@ -188,6 +189,7 @@ class MultiNamingStyleTest(CheckerTestCase): Message('invalid-name', node=classes[2], args=('class', 'CLASSC', ''))): for cls in classes: self.checker.visit_class(cls) + self.checker.leave_module(cls.root) @set_config(method_rgx=MULTI_STYLE_RE, function_rgx=MULTI_STYLE_RE, @@ -204,6 +206,7 @@ class MultiNamingStyleTest(CheckerTestCase): with self.assertAddsMessages(Message('invalid-name', node=function_defs[1], args=('function', 'FUNC', ''))): for func in function_defs: self.checker.visit_function(func) + self.checker.leave_module(func.root) @set_config(function_rgx=re.compile('(?:(?PFOO)|(?P[A-Z]+)|(?P[a-z]+))$')) def test_multi_name_detection_exempt(self): @@ -220,6 +223,7 @@ class MultiNamingStyleTest(CheckerTestCase): with self.assertAddsMessages(Message('invalid-name', node=function_defs[3], args=('function', 'UPPER', ''))): for func in function_defs: self.checker.visit_function(func) + self.checker.leave_module(func.root) if __name__ == '__main__': -- cgit v1.2.1 From d4695d4fd8b50df7840c6e7e4b4a9103c37388dc Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Sat, 16 Aug 2014 17:40:44 -0700 Subject: Move definition of Message class out of reporters module, and make it a namedtuple. --- reporters/__init__.py | 22 ---------------------- reporters/guireporter.py | 5 +++-- reporters/html.py | 6 ++++-- reporters/text.py | 14 +++++++++----- test/test_functional.py | 6 +++--- utils.py | 32 ++++++++++++++++++++++++++++---- 6 files changed, 47 insertions(+), 38 deletions(-) diff --git a/reporters/__init__.py b/reporters/__init__.py index 12d193f..28d7071 100644 --- a/reporters/__init__.py +++ b/reporters/__init__.py @@ -17,7 +17,6 @@ import sys import locale import os -from pylint.utils import MSG_TYPES from pylint import utils @@ -41,27 +40,6 @@ def diff_string(old, new): return diff_str -class Message(object): - """This class represent a message to be issued by the reporters""" - - def __init__(self, reporter, msg_id, location, msg): - self.msg_id = msg_id - self.abspath, self.module, self.obj, self.line, self.column = location - self.path = self.abspath.replace(reporter.path_strip_prefix, '') - self.msg = msg - self.C = msg_id[0] - self.category = MSG_TYPES[msg_id[0]] - self.symbol = reporter.linter.msgs_store.check_message_id(msg_id).symbol - - def format(self, template): - """Format the message according to the given template. - - The template format is the one of the format method : - cf. http://docs.python.org/2/library/string.html#formatstrings - """ - return template.format(**(self.__dict__)) - - class BaseReporter(object): """base class for reporters diff --git a/reporters/guireporter.py b/reporters/guireporter.py index 331eb17..8ebc270 100644 --- a/reporters/guireporter.py +++ b/reporters/guireporter.py @@ -3,7 +3,8 @@ import sys from pylint.interfaces import IReporter -from pylint.reporters import BaseReporter, Message +from pylint.reporters import BaseReporter +from pylint import utils from logilab.common.ureports import TextWriter @@ -20,7 +21,7 @@ class GUIReporter(BaseReporter): def add_message(self, msg_id, location, msg): """manage message of different type and in the context of path""" - message = Message(self, msg_id, location, msg) + message = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, location, msg) self.gui.msg_queue.put(message) def _display(self, layout): diff --git a/reporters/html.py b/reporters/html.py index 71d46eb..441f68a 100644 --- a/reporters/html.py +++ b/reporters/html.py @@ -18,8 +18,9 @@ from cgi import escape from logilab.common.ureports import HTMLWriter, Section, Table +from pylint import utils from pylint.interfaces import IReporter -from pylint.reporters import BaseReporter, Message +from pylint.reporters import BaseReporter class HTMLReporter(BaseReporter): @@ -35,7 +36,8 @@ class HTMLReporter(BaseReporter): def add_message(self, msg_id, location, msg): """manage message of different type and in the context of path""" - msg = Message(self, msg_id, location, msg) + msg = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, + location, msg) self.msgs += (msg.category, msg.module, msg.obj, str(msg.line), str(msg.column), escape(msg.msg)) diff --git a/reporters/text.py b/reporters/text.py index 04245f7..83bfe60 100644 --- a/reporters/text.py +++ b/reporters/text.py @@ -23,7 +23,8 @@ from logilab.common.ureports import TextWriter from logilab.common.textutils import colorize_ansi from pylint.interfaces import IReporter -from pylint.reporters import BaseReporter, Message +from pylint.reporters import BaseReporter +from pylint import utils TITLE_UNDERLINES = ['', '=', '-', '.'] @@ -50,7 +51,8 @@ class TextReporter(BaseReporter): def add_message(self, msg_id, location, msg): """manage message of different type and in the context of path""" - m = Message(self, msg_id, location, msg) + m = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, + location, msg) if m.module not in self._modules: if m.module: self.writeln('************* Module %s' % m.module) @@ -118,7 +120,7 @@ class ColorizedTextReporter(TextReporter): """manage message of different types, and colorize output using ansi escape codes """ - msg = Message(self, msg_id, location, msg) + msg = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, location, msg) if msg.module not in self._modules: color, style = self._get_decoration('S') if msg.module: @@ -130,8 +132,10 @@ class ColorizedTextReporter(TextReporter): self.writeln(modsep) self._modules.add(msg.module) color, style = self._get_decoration(msg.C) - for attr in ('msg', 'symbol', 'category', 'C'): - setattr(msg, attr, colorize_ansi(getattr(msg, attr), color, style)) + + msg = msg._replace( + **{attr: colorize_ansi(getattr(msg, attr), color, style) + for attr in ('msg', 'symbol', 'category', 'C')}) self.write_message(msg) diff --git a/test/test_functional.py b/test/test_functional.py index fb495e0..a6c6780 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -9,10 +9,10 @@ import re import sys import unittest +from pylint import checkers from pylint import lint from pylint import reporters -from pylint import checkers - +from pylint import utils class NoFileError(Exception): pass @@ -49,7 +49,7 @@ def parse_python_version(str): class TestReporter(reporters.BaseReporter): def add_message(self, msg_id, location, msg): - self.messages.append(reporters.Message(self, msg_id, location, msg)) + self.messages.append(utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, location, msg)) def on_set_current_module(self, module, filepath): self.messages = [] diff --git a/utils.py b/utils.py index 9a7189e..59a957a 100644 --- a/utils.py +++ b/utils.py @@ -17,11 +17,13 @@ main pylint class """ +import collections +import os import re import sys import tokenize -import os -from warnings import warn +import warnings + from os.path import dirname, basename, splitext, exists, isdir, join, normpath from logilab.common.interface import implements @@ -75,6 +77,28 @@ class WarningScope(object): LINE = 'line-based-msg' NODE = 'node-based-msg' +_MsgBase = collections.namedtuple( + '_MsgBase', + ['msg_id', 'symbol', 'msg', 'C', 'category', 'abspath', 'module', 'obj', + 'line', 'column']) + + +class Message(_MsgBase): + """This class represent a message to be issued by the reporters""" + def __new__(cls, msg_id, symbol, location, msg): + return _MsgBase.__new__( + cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], *location) + + def format(self, template): + """Format the message according to the given template. + + The template format is the one of the format method : + cf. http://docs.python.org/2/library/string.html#formatstrings + """ + # For some reason, _asdict on derived namedtuples does not work with + # Python 3.4. Needs some investigation. + return template.format(**dict(zip(self._fields, self))) + def get_module_and_frameid(node): """return the module name and the frame id in the module""" @@ -124,8 +148,8 @@ def build_message_def(checker, msgid, msg_tuple): # messages should have a symbol, but for backward compatibility # they may not. (msg, descr) = msg_tuple - warn("[pylint 0.26] description of message %s doesn't include " - "a symbolic name" % msgid, DeprecationWarning) + warnings.warn("[pylint 0.26] description of message %s doesn't include " + "a symbolic name" % msgid, DeprecationWarning) symbol = None options.setdefault('scope', default_scope) return MessageDefinition(checker, msgid, msg, descr, symbol, **options) -- cgit v1.2.1 From 20ffb646e24da344602c705be938f2f6baac0494 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Sat, 16 Aug 2014 17:57:50 -0700 Subject: Extend the reporter interface with a method that takes a message object, for better extendability. --- reporters/__init__.py | 11 +++++++++-- reporters/guireporter.py | 5 ++--- reporters/html.py | 4 +--- reporters/text.py | 18 +++++++----------- test/test_functional.py | 4 ++-- test/test_self.py | 4 ++-- utils.py | 4 +++- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/reporters/__init__.py b/reporters/__init__.py index 28d7071..f22653e 100644 --- a/reporters/__init__.py +++ b/reporters/__init__.py @@ -60,9 +60,16 @@ class BaseReporter(object): # Build the path prefix to strip to get relative paths self.path_strip_prefix = os.getcwd() + os.sep + def handle_message(self, msg): + """Handle a new message triggered on the current file. + + Invokes the legacy add_message API by default.""" + self.add_message( + msg.msg_id, (msg.abspath, msg.module, msg.obj, msg.line, msg.column), + msg.msg) + def add_message(self, msg_id, location, msg): - """Client API to send a message""" - # Shall we store the message objects somewhere, do some validity checking ? + """Deprecated, do not use.""" raise NotImplementedError def set_output(self, output=None): diff --git a/reporters/guireporter.py b/reporters/guireporter.py index 8ebc270..f908f76 100644 --- a/reporters/guireporter.py +++ b/reporters/guireporter.py @@ -19,10 +19,9 @@ class GUIReporter(BaseReporter): BaseReporter.__init__(self, output) self.gui = gui - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different type and in the context of path""" - message = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, location, msg) - self.gui.msg_queue.put(message) + self.gui.msg_queue.put(msg) def _display(self, layout): """launch layouts display""" diff --git a/reporters/html.py b/reporters/html.py index 441f68a..3e5a1a7 100644 --- a/reporters/html.py +++ b/reporters/html.py @@ -34,10 +34,8 @@ class HTMLReporter(BaseReporter): BaseReporter.__init__(self, output) self.msgs = [] - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different type and in the context of path""" - msg = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, - location, msg) self.msgs += (msg.category, msg.module, msg.obj, str(msg.line), str(msg.column), escape(msg.msg)) diff --git a/reporters/text.py b/reporters/text.py index 83bfe60..48b5155 100644 --- a/reporters/text.py +++ b/reporters/text.py @@ -24,7 +24,6 @@ from logilab.common.textutils import colorize_ansi from pylint.interfaces import IReporter from pylint.reporters import BaseReporter -from pylint import utils TITLE_UNDERLINES = ['', '=', '-', '.'] @@ -49,17 +48,15 @@ class TextReporter(BaseReporter): """Convenience method to write a formated message with class default template""" self.writeln(msg.format(self._template)) - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different type and in the context of path""" - m = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, - location, msg) - if m.module not in self._modules: - if m.module: - self.writeln('************* Module %s' % m.module) - self._modules.add(m.module) + if msg.module not in self._modules: + if msg.module: + self.writeln('************* Module %s' % msg.module) + self._modules.add(msg.module) else: self.writeln('************* ') - self.write_message(m) + self.write_message(msg) def _display(self, layout): """launch layouts display""" @@ -116,11 +113,10 @@ class ColorizedTextReporter(TextReporter): except KeyError: return None, None - def add_message(self, msg_id, location, msg): + def handle_message(self, msg): """manage message of different types, and colorize output using ansi escape codes """ - msg = utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, location, msg) if msg.module not in self._modules: color, style = self._get_decoration('S') if msg.module: diff --git a/test/test_functional.py b/test/test_functional.py index a6c6780..daf699f 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -48,8 +48,8 @@ def parse_python_version(str): class TestReporter(reporters.BaseReporter): - def add_message(self, msg_id, location, msg): - self.messages.append(utils.Message(msg_id, self.linter.msgs_store.check_message_id(msg_id).symbol, location, msg)) + def handle_message(self, msg): + self.messages.append(msg) def on_set_current_module(self, module, filepath): self.messages = [] diff --git a/test/test_self.py b/test/test_self.py index 4935f8a..7eb3261 100644 --- a/test/test_self.py +++ b/test/test_self.py @@ -33,9 +33,9 @@ class MultiReporter(BaseReporter): for rep in self._reporters: rep.on_set_current_module(*args, **kwargs) - def add_message(self, *args, **kwargs): + def handle_message(self, msg): for rep in self._reporters: - rep.add_message(*args, **kwargs) + rep.handle_message(msg) def display_results(self, layout): pass diff --git a/utils.py b/utils.py index 59a957a..0d2cd3c 100644 --- a/utils.py +++ b/utils.py @@ -376,7 +376,9 @@ class MessagesHandlerMixIn(object): module, obj = get_module_and_frameid(node) path = node.root().file # add the message - self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offset or 0), msg) + self.reporter.handle_message( + Message(msgid, symbol, + (path, module, obj, line or 1, col_offset or 0), msg)) def print_full_documentation(self): """output a full documentation in ReST format""" -- cgit v1.2.1 From a7d1515daef1234b3bf3e3991299771bea3c56bc Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Sat, 16 Aug 2014 20:16:08 -0700 Subject: Expected output files for the new test framework are now CSV files. CSV files are easier to parse (messages with multi-line output were parsed with a heuristic so far), and are easier to extend with new fields. --- test/functional/anomalous_unicode_escape.txt | 6 +- test/functional/bad_continuation.txt | 86 +++++++++++++-------------- test/functional/bad_open_mode.txt | 14 ++--- test/functional/class_members_py27.txt | 12 ++-- test/functional/docstrings.txt | 16 ++--- test/functional/exception_is_binary_op.txt | 8 +-- test/functional/future_unicode_literals.txt | 2 +- test/functional/import_error.txt | 4 +- test/functional/invalid_encoded_data.txt | 2 +- test/functional/invalid_exceptions_caught.txt | 6 +- test/functional/invalid_exceptions_raised.txt | 12 ++-- test/functional/invalid_name.txt | 4 +- test/functional/missing_self_argument.txt | 4 +- test/functional/name_styles.txt | 34 +++++------ test/functional/newstyle_properties.txt | 2 +- test/functional/statement_without_effect.txt | 72 +++++++++++++++------- test/functional/string_formatting.txt | 2 +- test/functional/too_many_branches.txt | 2 +- test/test_functional.py | 35 +++++++---- 19 files changed, 185 insertions(+), 138 deletions(-) diff --git a/test/functional/anomalous_unicode_escape.txt b/test/functional/anomalous_unicode_escape.txt index 5c1acc7..c242cb9 100644 --- a/test/functional/anomalous_unicode_escape.txt +++ b/test/functional/anomalous_unicode_escape.txt @@ -1,3 +1,3 @@ -anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. -anomalous-unicode-escape-in-string:6::Anomalous Unicode escape in byte string: '\U'. String constant might be missing an r or u prefix. -anomalous-unicode-escape-in-string:8::Anomalous Unicode escape in byte string: '\N'. String constant might be missing an r or u prefix. +anomalous-unicode-escape-in-string:5::"Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix." +anomalous-unicode-escape-in-string:6::"Anomalous Unicode escape in byte string: '\U'. String constant might be missing an r or u prefix." +anomalous-unicode-escape-in-string:8::"Anomalous Unicode escape in byte string: '\N'. String constant might be missing an r or u prefix." diff --git a/test/functional/bad_continuation.txt b/test/functional/bad_continuation.txt index 732858e..f970780 100644 --- a/test/functional/bad_continuation.txt +++ b/test/functional/bad_continuation.txt @@ -1,63 +1,63 @@ -bad-continuation:12::Wrong hanging indentation. +bad-continuation:12::"Wrong hanging indentation. ] # [bad-continuation] -| ^| -bad-continuation:17::Wrong continued indentation. +| ^|" +bad-continuation:17::"Wrong continued indentation. 7, # [bad-continuation] - | ^ -bad-continuation:25::Wrong hanging indentation. + | ^" +bad-continuation:25::"Wrong hanging indentation. 'b': 2, # [bad-continuation] - ^| -bad-continuation:31::Wrong hanging indentation. + ^|" +bad-continuation:31::"Wrong hanging indentation. 'b': 2, # [bad-continuation] - ^| -bad-continuation:39::Wrong continued indentation. + ^|" +bad-continuation:39::"Wrong continued indentation. # [bad-continuation] is not accepted - | | ^ -bad-continuation:40::Wrong continued indentation. + | | ^" +bad-continuation:40::"Wrong continued indentation. 'contents', # [bad-continuation] nor this. - | ^ -bad-continuation:49::Wrong hanging indentation in dict value. + | ^" +bad-continuation:49::"Wrong hanging indentation in dict value. 'value2', # [bad-continuation] - | ^ | -bad-continuation:59::Wrong continued indentation. + | ^ |" +bad-continuation:59::"Wrong continued indentation. 'wrong', # [bad-continuation] - ^ | -bad-continuation:83::Wrong hanging indentation in dict value. + ^ |" +bad-continuation:83::"Wrong hanging indentation in dict value. 'value1', # [bad-continuation] -^ | | -bad-continuation:87::Wrong hanging indentation in dict value. +^ | |" +bad-continuation:87::"Wrong hanging indentation in dict value. 'value1', # [bad-continuation] - ^ | | -bad-continuation:104::Wrong hanging indentation before block. + ^ | |" +bad-continuation:104::"Wrong hanging indentation before block. some_arg, # [bad-continuation] - ^ | -bad-continuation:105::Wrong hanging indentation before block. + ^ |" +bad-continuation:105::"Wrong hanging indentation before block. some_other_arg): # [bad-continuation] - ^ | -bad-continuation:125::Wrong continued indentation. - "b") # [bad-continuation] - ^ | -bad-continuation:139::Wrong hanging indentation before block. + ^ |" +bad-continuation:125::"Wrong continued indentation. + ""b"") # [bad-continuation] + ^ |" +bad-continuation:139::"Wrong hanging indentation before block. ): pass # [bad-continuation] -| ^| -bad-continuation:142::Wrong continued indentation before block. +| ^|" +bad-continuation:142::"Wrong continued indentation before block. 2): # [bad-continuation] - ^ | -bad-continuation:150::Wrong continued indentation. + ^ |" +bad-continuation:150::"Wrong continued indentation. 2 and # [bad-continuation] - | ^ -bad-continuation:155::Wrong hanging indentation before block. + | ^" +bad-continuation:155::"Wrong hanging indentation before block. 2): pass # [bad-continuation] - ^ | | -bad-continuation:162::Wrong continued indentation before block. + ^ | |" +bad-continuation:162::"Wrong continued indentation before block. 2 or # [bad-continuation] - |^ | -bad-continuation:166::Wrong continued indentation before block. + |^ |" +bad-continuation:166::"Wrong continued indentation before block. 2): pass # [bad-continuation] - ^ | | -bad-continuation:172::Wrong hanging indentation before block. + ^ | |" +bad-continuation:172::"Wrong hanging indentation before block. 2): # [bad-continuation] - ^ | | -bad-continuation:183::Wrong continued indentation. + ^ | |" +bad-continuation:183::"Wrong continued indentation. 2): # [bad-continuation] - ^ | + ^ |" diff --git a/test/functional/bad_open_mode.txt b/test/functional/bad_open_mode.txt index 356e02e..d0bb8bb 100644 --- a/test/functional/bad_open_mode.txt +++ b/test/functional/bad_open_mode.txt @@ -1,7 +1,7 @@ -bad-open-mode:4::"rw" is not a valid mode for open. -bad-open-mode:5::"rw" is not a valid mode for open. -bad-open-mode:6::"rw" is not a valid mode for open. -bad-open-mode:7::"U+" is not a valid mode for open. -bad-open-mode:8::"rb+" is not a valid mode for open. -bad-open-mode:9::"Uw" is not a valid mode for open. -bad-open-mode:13::"Uwz" is not a valid mode for open. +bad-open-mode:4::"""rw"" is not a valid mode for open." +bad-open-mode:5::"""rw"" is not a valid mode for open." +bad-open-mode:6::"""rw"" is not a valid mode for open." +bad-open-mode:7::"""U+"" is not a valid mode for open." +bad-open-mode:8::"""rb+"" is not a valid mode for open." +bad-open-mode:9::"""Uw"" is not a valid mode for open." +bad-open-mode:13::"""Uwz"" is not a valid mode for open." diff --git a/test/functional/class_members_py27.txt b/test/functional/class_members_py27.txt index 3e88b55..c7b017e 100644 --- a/test/functional/class_members_py27.txt +++ b/test/functional/class_members_py27.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member -no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member -no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member -no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member -no-member:50::Instance of 'TestMetaclass' has no 'register' member -no-member:51::Instance of 'UsingMetaclass' has no 'test' member +no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member +no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member +no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member +no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member +no-member:50::Instance of 'TestMetaclass' has no 'register' member +no-member:51::Instance of 'UsingMetaclass' has no 'test' member diff --git a/test/functional/docstrings.txt b/test/functional/docstrings.txt index 1ea5c7d..177d02a 100644 --- a/test/functional/docstrings.txt +++ b/test/functional/docstrings.txt @@ -1,8 +1,8 @@ -missing-docstring:1::Missing module docstring -empty-docstring:6:function0:Empty function docstring -missing-docstring:10:function1:Missing function docstring -missing-docstring:23:AAAA:Missing class docstring -missing-docstring:40:AAAA.method1:Missing method docstring -empty-docstring:48:AAAA.method3:Empty method docstring -empty-docstring:62:DDDD.method2:Empty method docstring -missing-docstring:70:DDDD.method4:Missing method docstring +missing-docstring:1::Missing module docstring +empty-docstring:6:function0:Empty function docstring +missing-docstring:10:function1:Missing function docstring +missing-docstring:23:AAAA:Missing class docstring +missing-docstring:40:AAAA.method1:Missing method docstring +empty-docstring:48:AAAA.method3:Empty method docstring +empty-docstring:62:DDDD.method2:Empty method docstring +missing-docstring:70:DDDD.method4:Missing method docstring diff --git a/test/functional/exception_is_binary_op.txt b/test/functional/exception_is_binary_op.txt index 039bc3f..e6512c9 100644 --- a/test/functional/exception_is_binary_op.txt +++ b/test/functional/exception_is_binary_op.txt @@ -1,4 +1,4 @@ -binary-op-exception:5::Exception to catch is the result of a binary "or" operation -binary-op-exception:7::Exception to catch is the result of a binary "and" operation -binary-op-exception:9::Exception to catch is the result of a binary "or" operation -binary-op-exception:11::Exception to catch is the result of a binary "or" operation +binary-op-exception:5::"Exception to catch is the result of a binary ""or"" operation" +binary-op-exception:7::"Exception to catch is the result of a binary ""and"" operation" +binary-op-exception:9::"Exception to catch is the result of a binary ""or"" operation" +binary-op-exception:11::"Exception to catch is the result of a binary ""or"" operation" diff --git a/test/functional/future_unicode_literals.txt b/test/functional/future_unicode_literals.txt index c490da5..60d291e 100644 --- a/test/functional/future_unicode_literals.txt +++ b/test/functional/future_unicode_literals.txt @@ -1 +1 @@ -anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. +anomalous-unicode-escape-in-string:5::"Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix." diff --git a/test/functional/import_error.txt b/test/functional/import_error.txt index af7f739..0602d2a 100644 --- a/test/functional/import_error.txt +++ b/test/functional/import_error.txt @@ -1,2 +1,2 @@ -import-error:3::Unable to import 'totally_missing' -import-error:16::Unable to import 'maybe_missing_2' +import-error:3::Unable to import 'totally_missing' +import-error:16::Unable to import 'maybe_missing_2' diff --git a/test/functional/invalid_encoded_data.txt b/test/functional/invalid_encoded_data.txt index dc23869..c6e69dc 100644 --- a/test/functional/invalid_encoded_data.txt +++ b/test/functional/invalid_encoded_data.txt @@ -1 +1 @@ -invalid-encoded-data:5::Cannot decode using encoding "utf-8", unexpected byte at position 11 +invalid-encoded-data:5::"Cannot decode using encoding ""utf-8"", unexpected byte at position 11" diff --git a/test/functional/invalid_exceptions_caught.txt b/test/functional/invalid_exceptions_caught.txt index 55b653b..3156a0b 100644 --- a/test/functional/invalid_exceptions_caught.txt +++ b/test/functional/invalid_exceptions_caught.txt @@ -1,6 +1,6 @@ -catching-non-exception:25::Catching an exception which doesn't inherit from BaseException: MyException -catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MyException -catching-non-exception:31::Catching an exception which doesn't inherit from BaseException: MySecondException +catching-non-exception:25::"Catching an exception which doesn't inherit from BaseException: MyException" +catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MyException" +catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MySecondException" catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: None catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: list() catching-non-exception:57::Catching an exception which doesn't inherit from BaseException: None diff --git a/test/functional/invalid_exceptions_raised.txt b/test/functional/invalid_exceptions_raised.txt index 756d92c..c40eca7 100644 --- a/test/functional/invalid_exceptions_raised.txt +++ b/test/functional/invalid_exceptions_raised.txt @@ -1,9 +1,13 @@ -raising-non-exception:23:bad_case0:Raising a new style class which doesn't inherit from BaseException -nonstandard-exception:23:bad_case0:Exception doesn't inherit from standard "Exception" class +nonstandard-exception:23:bad_case0:"Exception doesn't inherit from standard ""Exception"" class" +raising-non-exception:23:bad_case0:"""Raising a new style class which doesn't inherit from BaseException +"" +" raising-non-exception:27:bad_case1:Raising a new style class which doesn't inherit from BaseException -raising-non-exception:33:bad_case2:Raising a new style class which doesn't inherit from BaseException -nonstandard-exception:33:bad_case2:Exception doesn't inherit from standard "Exception" class +nonstandard-exception:33:bad_case2:"Exception doesn't inherit from standard ""Exception"" class" old-raise-syntax:33:bad_case2:Use raise ErrorClass(args) instead of raise ErrorClass, args. +raising-non-exception:33:bad_case2:"""Raising a new style class which doesn't inherit from BaseException +"" +" raising-non-exception:37:bad_case3:Raising a new style class which doesn't inherit from BaseException notimplemented-raised:42:bad_case4:NotImplemented raised - should raise NotImplementedError old-raise-syntax:42:bad_case4:Use raise ErrorClass(args) instead of raise ErrorClass, args. diff --git a/test/functional/invalid_name.txt b/test/functional/invalid_name.txt index c722861..0c8eafe 100644 --- a/test/functional/invalid_name.txt +++ b/test/functional/invalid_name.txt @@ -1,2 +1,2 @@ -invalid-name:10::Invalid constant name "aaa" -invalid-name:14::Invalid constant name "time" +invalid-name:10::"Invalid constant name ""aaa""" +invalid-name:14::"Invalid constant name ""time""" diff --git a/test/functional/missing_self_argument.txt b/test/functional/missing_self_argument.txt index 3f7f7f2..1deef18 100644 --- a/test/functional/missing_self_argument.txt +++ b/test/functional/missing_self_argument.txt @@ -1,4 +1,6 @@ no-method-argument:11:MyClass.method:Method has no argument -no-method-argument:13:MyClass.met:Method has no argument +no-method-argument:13:MyClass.met:"""Method has no argument +"" +" no-method-argument:14:MyClass.setup:Method has no argument undefined-variable:16:MyClass.setup:Undefined variable 'self' diff --git a/test/functional/name_styles.txt b/test/functional/name_styles.txt index 4f0ae53..985e9a2 100644 --- a/test/functional/name_styles.txt +++ b/test/functional/name_styles.txt @@ -1,17 +1,17 @@ -invalid-name:6::Invalid constant name "bad_const_name" -invalid-name:9:BADFUNCTION_name:Invalid function name "BADFUNCTION_name" -invalid-name:11:BADFUNCTION_name:Invalid variable name "BAD_LOCAL_VAR" -invalid-name:15:func_bad_argname:Invalid argument name "NOT_GOOD" -invalid-name:25:bad_class_name:Invalid class name "bad_class_name" -invalid-name:36:CorrectClassName.__init__:Invalid attribute name "_Bad_AtTR_name" -invalid-name:37:CorrectClassName.__init__:Invalid attribute name "Bad_PUBLIC_name" -invalid-name:39:CorrectClassName:Invalid class attribute name "zz" -invalid-name:42:CorrectClassName.BadMethodName:Invalid method name "BadMethodName" -invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:Invalid method name "__DunDER_IS_not_free_for_all__" -invalid-name:78::Invalid class name "BAD_NAME_FOR_CLASS" -invalid-name:79::Invalid class name "NEXT_BAD_NAME_FOR_CLASS" -invalid-name:86::Invalid class name "NOT_CORRECT" -invalid-name:92:test_globals:Invalid constant name "AlsoCorrect" -invalid-name:105:FooClass.PROPERTY_NAME:Invalid attribute name "PROPERTY_NAME" -invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:Invalid attribute name "ABSTRACT_PROPERTY_NAME" -invalid-name:115:FooClass.PROPERTY_NAME_SETTER:Invalid attribute name "PROPERTY_NAME_SETTER" +invalid-name:6::"Invalid constant name ""bad_const_name""" +invalid-name:9:BADFUNCTION_name:"Invalid function name ""BADFUNCTION_name""" +invalid-name:11:BADFUNCTION_name:"Invalid variable name ""BAD_LOCAL_VAR""" +invalid-name:15:func_bad_argname:"Invalid argument name ""NOT_GOOD""" +invalid-name:25:bad_class_name:"Invalid class name ""bad_class_name""" +invalid-name:36:CorrectClassName.__init__:"Invalid attribute name ""_Bad_AtTR_name""" +invalid-name:37:CorrectClassName.__init__:"Invalid attribute name ""Bad_PUBLIC_name""" +invalid-name:39:CorrectClassName:"Invalid class attribute name ""zz""" +invalid-name:42:CorrectClassName.BadMethodName:"Invalid method name ""BadMethodName""" +invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:"Invalid method name ""__DunDER_IS_not_free_for_all__""" +invalid-name:78::"Invalid class name ""BAD_NAME_FOR_CLASS""" +invalid-name:79::"Invalid class name ""NEXT_BAD_NAME_FOR_CLASS""" +invalid-name:86::"Invalid class name ""NOT_CORRECT""" +invalid-name:92:test_globals:"Invalid constant name ""AlsoCorrect""" +invalid-name:105:FooClass.PROPERTY_NAME:"Invalid attribute name ""PROPERTY_NAME""" +invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:"Invalid attribute name ""ABSTRACT_PROPERTY_NAME""" +invalid-name:115:FooClass.PROPERTY_NAME_SETTER:"Invalid attribute name ""PROPERTY_NAME_SETTER""" diff --git a/test/functional/newstyle_properties.txt b/test/functional/newstyle_properties.txt index f3a8dbb..9f060c0 100644 --- a/test/functional/newstyle_properties.txt +++ b/test/functional/newstyle_properties.txt @@ -1,2 +1,2 @@ old-style-class:13:OldStyleClass:Old-style class defined. -property-on-old-class:15:OldStyleClass:Use of "property" on an old style class +property-on-old-class:15:OldStyleClass:"Use of ""property"" on an old style class" diff --git a/test/functional/statement_without_effect.txt b/test/functional/statement_without_effect.txt index 636a441..81fc51b 100644 --- a/test/functional/statement_without_effect.txt +++ b/test/functional/statement_without_effect.txt @@ -1,28 +1,60 @@ pointless-string-statement:5::String statement has no effect -pointless-statement:6::Statement seems to have no effect -pointless-statement:8::Statement seems to have no effect +pointless-statement:6::"""Statement seems to have no effect +"" +" +pointless-statement:8::"""Statement seems to have no effect +"" +" pointless-statement:9::Statement seems to have no effect pointless-statement:11::Statement seems to have no effect -pointless-statement:12::Statement seems to have no effect +pointless-statement:12::"""Statement seems to have no effect +"" +" pointless-statement:15::Statement seems to have no effect -pointless-string-statement:15::String statement has no effect -unnecessary-semicolon:17::Unnecessary semicolon +pointless-string-statement:15::"""String statement has no effect +"" +" +unnecessary-semicolon:17::"""Unnecessary semicolon +"" +" pointless-string-statement:18::String statement has no effect -unnecessary-semicolon:18::Unnecessary semicolon -expression-not-assigned:19::Expression "(list()) and (tuple())" is assigned to nothing -expression-not-assigned:20::Expression "(list()) and (tuple())" is assigned to nothing +unnecessary-semicolon:18::"""Unnecessary semicolon +"" +" +expression-not-assigned:19::"""Expression """"(list()) and (tuple())"""" is assigned to nothing +"" +" +expression-not-assigned:20::"""Expression """"(list()) and (tuple())"""" is assigned to nothing +"" +" unnecessary-semicolon:21::Unnecessary semicolon -expression-not-assigned:23::Expression "(list()) and (tuple())" is assigned to nothing -expression-not-assigned:26::Expression "ANSWER == to_be()" is assigned to nothing -expression-not-assigned:27::Expression "ANSWER == to_be()" is assigned to nothing -expression-not-assigned:28::Expression "(to_be()) or (not to_be())" is assigned to nothing -expression-not-assigned:29::Expression "(to_be()) or (not to_be())" is assigned to nothing -expression-not-assigned:30::Expression "ANSWER == to_be()" is assigned to nothing -expression-not-assigned:32::Expression "(to_be()) or (not to_be())" is assigned to nothing -expression-not-assigned:33::Expression "to_be().title" is assigned to nothing -pointless-string-statement:54:ClassLevelAttributeTest.__init__:String statement has no effect -pointless-string-statement:55:ClassLevelAttributeTest.__init__:String statement has no effect +expression-not-assigned:23::"Expression ""(list()) and (tuple())"" is assigned to nothing" +expression-not-assigned:26::"""Expression """"ANSWER == to_be()"""" is assigned to nothing +"" +" +expression-not-assigned:27::"""Expression """"ANSWER == to_be()"""" is assigned to nothing +"" +" +expression-not-assigned:28::"""Expression """"(to_be()) or (not to_be())"""" is assigned to nothing +"" +" +expression-not-assigned:29::"""Expression """"(to_be()) or (not to_be())"""" is assigned to nothing +"" +" +expression-not-assigned:30::"Expression ""ANSWER == to_be()"" is assigned to nothing" +expression-not-assigned:32::"Expression ""(to_be()) or (not to_be())"" is assigned to nothing" +expression-not-assigned:33::"Expression ""to_be().title"" is assigned to nothing" +pointless-string-statement:54:ClassLevelAttributeTest.__init__:"""String statement has no effect +"" +" +pointless-string-statement:55:ClassLevelAttributeTest.__init__:"""String statement has no effect +"" +" pointless-string-statement:58:ClassLevelAttributeTest.__init__:String statement has no effect -pointless-string-statement:61:ClassLevelAttributeTest.test:String statement has no effect -pointless-string-statement:62:ClassLevelAttributeTest.test:String statement has no effect +pointless-string-statement:61:ClassLevelAttributeTest.test:"""String statement has no effect +"" +" +pointless-string-statement:62:ClassLevelAttributeTest.test:"""String statement has no effect +"" +" pointless-string-statement:65:ClassLevelAttributeTest.test:String statement has no effect diff --git a/test/functional/string_formatting.txt b/test/functional/string_formatting.txt index 5f27835..593c161 100644 --- a/test/functional/string_formatting.txt +++ b/test/functional/string_formatting.txt @@ -16,7 +16,7 @@ missing-format-argument-key:71:pprint_bad:Missing keyword argument 'b' for forma missing-format-argument-key:72:pprint_bad:Missing keyword argument 'a' for format string missing-format-attribute:74:pprint_bad:Missing format attribute 'length' in format specifier 'a.ids.__len__.length' invalid-format-index:75:pprint_bad:Using invalid lookup key 400 in format specifier 'a.ids[3][400]' -invalid-format-index:76:pprint_bad:Using invalid lookup key "'string'" in format specifier 'a.ids[3]["\'string\'"]' +invalid-format-index:76:pprint_bad:"Using invalid lookup key ""'string'"" in format specifier 'a.ids[3][""\'string\'""]'" invalid-format-index:77:pprint_bad:Using invalid lookup key 1 in format specifier '0[0][1]' invalid-format-index:78:pprint_bad:Using invalid lookup key 0 in format specifier '0[0][0]' missing-format-argument-key:80:pprint_bad:Missing keyword argument 'b' for format string diff --git a/test/functional/too_many_branches.txt b/test/functional/too_many_branches.txt index 434dc3e..fbc82cc 100644 --- a/test/functional/too_many_branches.txt +++ b/test/functional/too_many_branches.txt @@ -1 +1 @@ -too-many-branches:3:wrong:Too many branches (13/12) +too-many-branches:3:wrong:Too many branches (13/12) diff --git a/test/test_functional.py b/test/test_functional.py index daf699f..759a447 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -1,5 +1,6 @@ """Functional full-module tests for PyLint.""" from __future__ import unicode_literals +import csv import collections import ConfigParser import io @@ -14,6 +15,18 @@ from pylint import lint from pylint import reporters from pylint import utils +class test_dialect(csv.excel): + if sys.version_info[0] < 3: + delimiter = b':' + lineterminator = b'\n' + else: + delimiter = ':' + lineterminator = '\n' + + +csv.register_dialect('test', test_dialect) + + class NoFileError(Exception): pass @@ -27,7 +40,11 @@ class OutputLine(collections.namedtuple('OutputLine', ['symbol', 'lineno', 'object', 'msg'])): @classmethod def from_msg(cls, msg): - return cls(msg.symbol, msg.line, msg.obj or '', msg.msg + '\n') + return cls(msg.symbol, msg.line, msg.obj or '', msg.msg) + + @classmethod + def from_csv(cls, row): + return cls(row[0], int(row[1]), row[2], row[3]) # Common sub-expressions. @@ -123,16 +140,7 @@ _OPERATORS = { } def parse_expected_output(stream): - lines = [] - for line in stream: - parts = line.split(':', 3) - if len(parts) != 4: - symbol, lineno, obj, msg = lines.pop() - lines.append(OutputLine(symbol, lineno, obj, msg + line)) - else: - linenum = int(parts[1]) - lines.append(OutputLine(parts[0], linenum, parts[2], parts[3])) - return lines + return [OutputLine.from_csv(row) for row in csv.reader(stream, 'test')] def get_expected_messages(stream): @@ -295,9 +303,10 @@ class LintModuleOutputUpdate(LintModuleTest): remaining.extend(received_lines) remaining.sort(key=lambda m: (m[1], m[0], m[3])) with open(self._test_file.expected_output, 'w') as fobj: + writer = csv.writer(fobj, dialect='test') for line in remaining: - fobj.write('{0}:{1}:{2}:{3}'.format(*line)) - + #fobj.write('{0}:{1}:{2}:{3}'.format(*line)) + writer.writerow(line) def suite(): input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), -- cgit v1.2.1 From 7aa57622583caab7dc14d0458bc3c4082036b766 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 24 Jul 2014 15:07:51 +0200 Subject: Implement confidence levels. - attach confidence levels to a number of messages - include confidence levels in the message object - if the confidence of a message is not HIGH or UNDEFINED, include it in the test output. --- ChangeLog | 5 ++ checkers/__init__.py | 6 ++- checkers/base.py | 46 +++++++++++------ checkers/exceptions.py | 8 +-- checkers/newstyle.py | 29 ++++++++--- checkers/typecheck.py | 22 +++----- checkers/utils.py | 19 +++++++ checkers/variables.py | 11 ++-- interfaces.py | 12 +++++ lint.py | 22 +++++++- test/functional/access_to__name__.txt | 4 +- test/functional/class_members_py27.txt | 12 ++--- test/functional/class_members_py30.txt | 12 ++--- test/functional/confidence_filter.py | 14 +++++ test/functional/confidence_filter.rc | 3 ++ test/functional/confidence_filter.txt | 1 + test/functional/docstrings.txt | 8 +-- test/functional/invalid_exceptions_caught.txt | 6 +-- test/functional/invalid_exceptions_raised.txt | 12 ++--- test/functional/member_checks.py | 63 ++++++++++++++++++++++ test/functional/member_checks.txt | 9 ++++ test/functional/name_styles.txt | 10 ++-- test/functional/namedtuple_member_inference.txt | 4 +- test/functional/newstyle__slots__.txt | 2 +- test/functional/newstyle_properties.txt | 2 +- test/functional/super_checks.txt | 14 ++--- test/input/func_typecheck_getattr.py | 69 ------------------------- test/messages/func_typecheck_getattr.txt | 10 ---- test/test_functional.py | 26 +++++++--- test/unittest_lint.py | 20 ++++--- test/unittest_utils.py | 2 +- testutils.py | 2 +- utils.py | 50 ++++++++++-------- 33 files changed, 329 insertions(+), 206 deletions(-) create mode 100644 test/functional/confidence_filter.py create mode 100644 test/functional/confidence_filter.rc create mode 100644 test/functional/confidence_filter.txt create mode 100644 test/functional/member_checks.py create mode 100644 test/functional/member_checks.txt delete mode 100644 test/input/func_typecheck_getattr.py delete mode 100644 test/messages/func_typecheck_getattr.txt diff --git a/ChangeLog b/ChangeLog index 3572212..2897f2f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -2,6 +2,11 @@ ChangeLog for Pylint ==================== -- + * Each message now comes with a confidence level attached, and + can be filtered base on this level. This allows to filter out + all messages that were emitted even though an inference failure + happened during checking. + * Improved presenting unused-import message. Closes issue #293. * Add new checker for finding spelling errors. New messages: diff --git a/checkers/__init__.py b/checkers/__init__.py index 693a5ff..8710f4a 100644 --- a/checkers/__init__.py +++ b/checkers/__init__.py @@ -46,6 +46,8 @@ from logilab.common.configuration import OptionsProviderMixIn from pylint.reporters import diff_string from pylint.utils import register_plugins +from pylint.interfaces import UNDEFINED + def table_lines_from_stats(stats, old_stats, columns): """get values listed in from and , @@ -90,9 +92,9 @@ class BaseChecker(OptionsProviderMixIn): OptionsProviderMixIn.__init__(self) self.linter = linter - def add_message(self, msg_id, line=None, node=None, args=None): + def add_message(self, msg_id, line=None, node=None, args=None, confidence=UNDEFINED): """add a message of a given type""" - self.linter.add_message(msg_id, line, node, args) + self.linter.add_message(msg_id, line, node, args, confidence) # dummy methods implementing the IChecker interface diff --git a/checkers/base.py b/checkers/base.py index d6553ed..298ac5f 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -24,7 +24,7 @@ from logilab.common.ureports import Table from astroid import are_exclusive, InferenceError import astroid.bases -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.utils import EmptyReport from pylint.reporters import diff_string from pylint.checkers import BaseChecker @@ -36,6 +36,7 @@ from pylint.checkers.utils import ( overrides_a_method, safe_infer, get_argument_from_call, + has_known_bases, NoSuchArgumentError, is_import_error, ) @@ -995,10 +996,15 @@ class NameChecker(_BasicChecker): def visit_function(self, node): # Do not emit any warnings if the method is just an implementation # of a base class method. - if node.is_method() and overrides_a_method(node.parent.frame(), node.name): - return + confidence = HIGH + if node.is_method(): + if overrides_a_method(node.parent.frame(), node.name): + return + confidence = (INFERENCE if has_known_bases(node.parent.frame()) + else INFERENCE_FAILURE) + self._check_name(_determine_function_name_type(node), - node.name, node) + node.name, node, confidence) # Check argument names args = node.args.args if args is not None: @@ -1047,20 +1053,22 @@ class NameChecker(_BasicChecker): def _find_name_group(self, node_type): return self._name_group.get(node_type, node_type) - def _is_multi_naming_match(self, match): + def _is_multi_naming_match(self, match, node_type, confidence): return (match is not None and match.lastgroup is not None and - match.lastgroup not in EXEMPT_NAME_CATEGORIES) + match.lastgroup not in EXEMPT_NAME_CATEGORIES + and (node_type != 'method' or confidence != INFERENCE_FAILURE)) - def _raise_name_warning(self, node, node_type, name): + def _raise_name_warning(self, node, node_type, name, confidence): 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.add_message('invalid-name', node=node, args=(type_label, name, hint), + confidence=confidence) self.stats['badname_' + node_type] += 1 - def _check_name(self, node_type, name, node): + def _check_name(self, node_type, name, node, confidence=HIGH): """check for a name using the type's regexp""" if is_inside_except(node): clobbering, _ = clobber_in_except(node) @@ -1075,14 +1083,14 @@ class NameChecker(_BasicChecker): regexp = getattr(self.config, node_type + '_rgx') match = regexp.match(name) - if self._is_multi_naming_match(match): + if self._is_multi_naming_match(match, node_type, confidence): name_group = self._find_name_group(node_type) bad_name_group = self._bad_names.setdefault(name_group, {}) warnings = bad_name_group.setdefault(match.lastgroup, []) - warnings.append((node, node_type, name)) + warnings.append((node, node_type, name, confidence)) if match is None: - self._raise_name_warning(node, node_type, name) + self._raise_name_warning(node, node_type, name, confidence) class DocStringChecker(_BasicChecker): @@ -1133,6 +1141,8 @@ class DocStringChecker(_BasicChecker): ftype = node.is_method() and 'method' or 'function' if isinstance(node.parent.frame(), astroid.Class): overridden = False + confidence = (INFERENCE if has_known_bases(node.parent.frame()) + else INFERENCE_FAILURE) # check if node is from a method overridden by its ancestor for ancestor in node.parent.frame().ancestors(): if node.name in ancestor and \ @@ -1140,11 +1150,13 @@ class DocStringChecker(_BasicChecker): overridden = True break self._check_docstring(ftype, node, - report_missing=not overridden) + report_missing=not overridden, + confidence=confidence) else: self._check_docstring(ftype, node) - def _check_docstring(self, node_type, node, report_missing=True): + def _check_docstring(self, node_type, node, report_missing=True, + confidence=HIGH): """check the node has a non empty docstring""" docstring = node.doc if docstring is None: @@ -1170,10 +1182,12 @@ class DocStringChecker(_BasicChecker): return elif func.bound.name in ('str', 'unicode', 'bytes'): return - self.add_message('missing-docstring', node=node, args=(node_type,)) + self.add_message('missing-docstring', node=node, args=(node_type,), + confidence=confidence) elif not docstring.strip(): self.stats['undocumented_'+node_type] += 1 - self.add_message('empty-docstring', node=node, args=(node_type,)) + self.add_message('empty-docstring', node=node, args=(node_type,), + confidence=confidence) class PassChecker(_BasicChecker): diff --git a/checkers/exceptions.py b/checkers/exceptions.py index cd78719..9b1a071 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -25,8 +25,8 @@ from pylint.checkers import BaseChecker from pylint.checkers.utils import ( is_empty, is_raising, check_messages, inherit_from_std_ex, - EXCEPTIONS_MODULE) -from pylint.interfaces import IAstroidChecker + EXCEPTIONS_MODULE, has_known_bases) +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE def _annotated_unpack_infer(stmt, context=None): """ @@ -234,7 +234,9 @@ class ExceptionsChecker(BaseChecker): if expr.newstyle: self.add_message('raising-non-exception', node=node) else: - self.add_message('nonstandard-exception', node=node) + self.add_message( + 'nonstandard-exception', node=node, + confidence=INFERENCE if has_known_bases(expr) else INFERENCE_FAILURE) else: value_found = False else: diff --git a/checkers/newstyle.py b/checkers/newstyle.py index cf50229..1946f94 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -19,9 +19,9 @@ import sys import astroid -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.checkers import BaseChecker -from pylint.checkers.utils import check_messages +from pylint.checkers.utils import check_messages, has_known_bases MSGS = { 'E1001': ('Use of __slots__ on an old style class', @@ -78,12 +78,17 @@ class NewStyleConflictChecker(BaseChecker): style class definition. """ if '__slots__' in node and not node.newstyle: - self.add_message('slots-on-old-class', node=node) + confidence = (INFERENCE if has_known_bases(node) + else INFERENCE_FAILURE) + self.add_message('slots-on-old-class', node=node, + confidence=confidence) # The node type could be class, exception, metaclass, or # interface. Presumably, the non-class-type nodes would always # have an explicit base class anyway. if not node.bases and node.type == 'class' and not node.metaclass(): - self.add_message('old-style-class', node=node) + # We use confidence HIGH here because this message should only ever + # be emitted for classes at the root of the inheritance hierarchyself. + self.add_message('old-style-class', node=node, confidence=HIGH) @check_messages('property-on-old-class') def visit_callfunc(self, node): @@ -92,9 +97,12 @@ class NewStyleConflictChecker(BaseChecker): if (isinstance(parent, astroid.Class) and not parent.newstyle and isinstance(node.func, astroid.Name)): + confidence = (INFERENCE if has_known_bases(parent) + else INFERENCE_FAILURE) name = node.func.name if name == 'property': - self.add_message('property-on-old-class', node=node) + self.add_message('property-on-old-class', node=node, + confidence=confidence) @check_messages('super-on-old-class', 'bad-super-call', 'missing-super-argument') def visit_function(self, node): @@ -112,9 +120,12 @@ class NewStyleConflictChecker(BaseChecker): if isinstance(call, astroid.CallFunc) and \ isinstance(call.func, astroid.Name) and \ call.func.name == 'super': + confidence = (INFERENCE if has_known_bases(klass) + else INFERENCE_FAILURE) if not klass.newstyle: # super should not be used on an old style class - self.add_message('super-on-old-class', node=node) + self.add_message('super-on-old-class', node=node, + confidence=confidence) else: # super first arg should be the class if not call.args and sys.version_info[0] == 3: @@ -128,7 +139,8 @@ class NewStyleConflictChecker(BaseChecker): continue if supcls is None: - self.add_message('missing-super-argument', node=call) + self.add_message('missing-super-argument', node=call, + confidence=confidence) continue if klass is not supcls: @@ -144,7 +156,8 @@ class NewStyleConflictChecker(BaseChecker): if name is not None: self.add_message('bad-super-call', node=call, - args=(name, )) + args=(name, ), + confidence=confidence) def register(linter): diff --git a/checkers/typecheck.py b/checkers/typecheck.py index a8851a2..27ed9f5 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -23,23 +23,19 @@ import astroid from astroid import InferenceError, NotFoundError, YES, Instance from astroid.bases import BUILTINS -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE from pylint.checkers import BaseChecker from pylint.checkers.utils import safe_infer, is_super, check_messages MSGS = { 'E1101': ('%s %r has no %r member', 'no-member', - 'Used when a variable is accessed for an unexistent member.'), + 'Used when a variable is accessed for an unexistent member.', + {'old_names': [('E1103', 'maybe-no-member')]}), 'E1102': ('%s is not callable', 'not-callable', 'Used when an object being called has been inferred to a non \ callable object'), - 'E1103': ('%s %r has no %r member (but some types could not be inferred)', - 'maybe-no-member', - 'Used when a variable is accessed for an unexistent member, but \ - astroid was not able to interpret all possible types of this \ - variable.'), 'E1111': ('Assigning to function call which doesn\'t return', 'assignment-from-no-return', 'Used when an assignment is done on a function call but the \ @@ -187,7 +183,7 @@ accessed. Python regular expressions are accepted.'} def visit_delattr(self, node): self.visit_getattr(node) - @check_messages('no-member', 'maybe-no-member') + @check_messages('no-member') def visit_getattr(self, node): """check that the accessed attribute exists @@ -279,13 +275,11 @@ accessed. Python regular expressions are accepted.'} if actual in done: continue done.add(actual) - if inference_failure: - msgid = 'maybe-no-member' - else: - msgid = 'no-member' - self.add_message(msgid, node=node, + confidence = INFERENCE if not inference_failure else INFERENCE_FAILURE + self.add_message('no-member', node=node, args=(owner.display_type(), name, - node.attrname)) + node.attrname), + confidence=confidence) @check_messages('assignment-from-no-return', 'assignment-from-none') def visit_assign(self, node): diff --git a/checkers/utils.py b/checkers/utils.py index 000aee7..b3ba51b 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -462,3 +462,22 @@ def is_import_error(handler): return True except astroid.InferenceError: continue + +def has_known_bases(klass): + """Returns true if all base classes of a class could be inferred.""" + try: + return klass._all_bases_known + except AttributeError: + pass + try: + for base in klass.bases: + result = base.infer().next() + # TODO: check for A->B->A->B pattern in class structure too? + if not isinstance(result, astroid.Class) or result is klass or not has_known_bases(result): + klass._all_bases_known = False + return False + except astroid.InferenceError: + klass._all_bases_known = False + return False + klass._all_bases_known = True + return True diff --git a/checkers/variables.py b/checkers/variables.py index a88bf3b..9c0b15c 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -25,14 +25,14 @@ from astroid import are_exclusive, builtin_lookup, AstroidBuildingException from logilab.common.modutils import file_from_modpath -from pylint.interfaces import IAstroidChecker +from pylint.interfaces import IAstroidChecker, INFERENCE, INFERENCE_FAILURE, HIGH from pylint.utils import get_global_option from pylint.checkers import BaseChecker from pylint.checkers.utils import ( PYMETHODS, is_ancestor_name, is_builtin, is_defined_before, is_error, is_func_default, is_func_decorator, assign_parent, check_messages, is_inside_except, clobber_in_except, - get_all_elements) + get_all_elements, has_known_bases) SPECIAL_OBJ = re.compile("^_{2}[a-z]+_{2}$") @@ -486,6 +486,10 @@ builtins. Remember that you should avoid to define new builtins when possible.' klass = node.parent.frame() if is_method and (klass.type == 'interface' or node.is_abstract()): return + if is_method and isinstance(klass, astroid.Class): + confidence = INFERENCE if has_known_bases(klass) else INFERENCE_FAILURE + else: + confidence = HIGH authorized_rgx = self.config.dummy_variables_rgx called_overridden = False argnames = node.argnames() @@ -539,7 +543,8 @@ builtins. Remember that you should avoid to define new builtins when possible.' # don't check callback arguments XXX should be configurable if node.name.startswith('cb_') or node.name.endswith('_cb'): continue - self.add_message('unused-argument', args=name, node=stmt) + self.add_message('unused-argument', args=name, node=stmt, + confidence=confidence) else: if stmt.parent and isinstance(stmt.parent, astroid.Assign): if name in nonlocal_names: diff --git a/interfaces.py b/interfaces.py index 50f2c83..ea3b40f 100644 --- a/interfaces.py +++ b/interfaces.py @@ -11,9 +11,21 @@ # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """Interfaces for PyLint objects""" +from collections import namedtuple from logilab.common.interface import Interface +Confidence = namedtuple('Confidence', ['name', 'description']) +# Warning Certainties +HIGH = Confidence('HIGH', 'No false positive possible.') +INFERENCE = Confidence('INFERENCE', 'Warning based on inference result.') +INFERENCE_FAILURE = Confidence('INFERENCE_FAILURE', + 'Warning based on inference with failures.') +UNDEFINED = Confidence('UNDEFINED', + 'Warning without any associated confidence level.') + +CONFIDENCE_LEVELS = [HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED] + class IChecker(Interface): """This is an base interface, not designed to be used elsewhere than for diff --git a/lint.py b/lint.py index 5be37f3..0f5aa6f 100644 --- a/lint.py +++ b/lint.py @@ -51,7 +51,7 @@ from pylint.utils import ( PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, MessagesStore, FileState, EmptyReport, expand_modules, tokenize_module) -from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker +from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker, CONFIDENCE_LEVELS from pylint.checkers import (BaseTokenChecker, table_lines_from_stats, initialize as checkers_initialize) @@ -238,6 +238,15 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, 'help' : 'Add a comment according to your evaluation note. ' 'This is used by the global evaluation report (RP0004).'}), + ('confidence', + {'type' : 'multiple_choice', 'metavar': '', + 'default': '', + 'choices': [c.name for c in CONFIDENCE_LEVELS], + 'group': 'Messages control', + 'help' : 'Only show warnings with the listed confidence levels.' + ' Leave empty to show all. Valid levels: %s' % ( + ', '.join(c.name for c in CONFIDENCE_LEVELS),)}), + ('enable', {'type' : 'csv', 'metavar': '', 'short': 'e', @@ -879,6 +888,12 @@ group are mutually exclusive.'), 'group': 'Commands', 'level': 1, 'help' : "Generate pylint's messages."}), + ('list-conf-levels', + {'action' : 'callback', + 'callback' : self.cb_list_confidence_levels, + 'group': 'Commands', 'level': 1, + 'help' : "Generate pylint's messages."}), + ('full-documentation', {'action' : 'callback', 'metavar': '', 'callback' : self.cb_full_documentation, @@ -1037,6 +1052,11 @@ group are mutually exclusive.'), self.linter.msgs_store.list_messages() sys.exit(0) + def cb_list_confidence_levels(self, option, optname, value, parser): + for level in CONFIDENCE_LEVELS: + print '%-18s: %s' % level + sys.exit(0) + def cb_init_hook(optname, value): """exec arbitrary code to set sys.path for instance""" exec value diff --git a/test/functional/access_to__name__.txt b/test/functional/access_to__name__.txt index 2c0a953..ecf5ffd 100644 --- a/test/functional/access_to__name__.txt +++ b/test/functional/access_to__name__.txt @@ -1,3 +1,3 @@ old-style-class:7:Aaaa:Old-style class defined. -no-member:10:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member -no-member:21:NewClass.__init__:Instance of 'NewClass' has no '__name__' member +no-member:10:Aaaa.__init__:Instance of 'Aaaa' has no '__name__' member:INFERENCE +no-member:21:NewClass.__init__:Instance of 'NewClass' has no '__name__' member:INFERENCE diff --git a/test/functional/class_members_py27.txt b/test/functional/class_members_py27.txt index c7b017e..e5e6005 100644 --- a/test/functional/class_members_py27.txt +++ b/test/functional/class_members_py27.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member -no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member -no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member -no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member -no-member:50::Instance of 'TestMetaclass' has no 'register' member -no-member:51::Instance of 'UsingMetaclass' has no 'test' member +no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member:INFERENCE +no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member:INFERENCE +no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member:INFERENCE +no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member:INFERENCE +no-member:50::Instance of 'TestMetaclass' has no 'register' member:INFERENCE +no-member:51::Instance of 'UsingMetaclass' has no 'test' member:INFERENCE diff --git a/test/functional/class_members_py30.txt b/test/functional/class_members_py30.txt index 0cb808f..4696579 100644 --- a/test/functional/class_members_py30.txt +++ b/test/functional/class_members_py30.txt @@ -1,6 +1,6 @@ -no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member -no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member -no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member -no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member -no-member:48::Instance of 'TestMetaclass' has no 'register' member -no-member:49::Instance of 'UsingMetaclass' has no 'test' member +no-member:14:MyClass.test:Instance of 'MyClass' has no 'incorrect' member:INFERENCE +no-member:15:MyClass.test:Instance of 'MyClass' has no 'havenot' member:INFERENCE +no-member:16:MyClass.test:Instance of 'MyClass' has no 'nonexistent1' member:INFERENCE +no-member:17:MyClass.test:Instance of 'MyClass' has no 'nonexistent2' member:INFERENCE +no-member:48::Instance of 'TestMetaclass' has no 'register' member:INFERENCE +no-member:49::Instance of 'UsingMetaclass' has no 'test' member:INFERENCE diff --git a/test/functional/confidence_filter.py b/test/functional/confidence_filter.py new file mode 100644 index 0000000..64cc8c4 --- /dev/null +++ b/test/functional/confidence_filter.py @@ -0,0 +1,14 @@ +"""Test for the confidence filter.""" + +class Client(object): + """use provider class""" + + def __init__(self): + self.set_later = 0 + + def set_set_later(self, value): + """set set_later attribute (introduce an inference ambiguity)""" + self.set_later = value + +print Client().set_later.lower() +print Client().foo # [no-member] diff --git a/test/functional/confidence_filter.rc b/test/functional/confidence_filter.rc new file mode 100644 index 0000000..5d21cb5 --- /dev/null +++ b/test/functional/confidence_filter.rc @@ -0,0 +1,3 @@ +[Messages Control] +disable=no-init,too-few-public-methods,undefined-variable +confidence=INFERENCE,HIGH,UNDEFINED diff --git a/test/functional/confidence_filter.txt b/test/functional/confidence_filter.txt new file mode 100644 index 0000000..308035d --- /dev/null +++ b/test/functional/confidence_filter.txt @@ -0,0 +1 @@ +no-member:14::Instance of 'Client' has no 'foo' member:INFERENCE diff --git a/test/functional/docstrings.txt b/test/functional/docstrings.txt index 177d02a..e30e462 100644 --- a/test/functional/docstrings.txt +++ b/test/functional/docstrings.txt @@ -2,7 +2,7 @@ missing-docstring:1::Missing module docstring empty-docstring:6:function0:Empty function docstring missing-docstring:10:function1:Missing function docstring missing-docstring:23:AAAA:Missing class docstring -missing-docstring:40:AAAA.method1:Missing method docstring -empty-docstring:48:AAAA.method3:Empty method docstring -empty-docstring:62:DDDD.method2:Empty method docstring -missing-docstring:70:DDDD.method4:Missing method docstring +missing-docstring:40:AAAA.method1:Missing method docstring:INFERENCE +empty-docstring:48:AAAA.method3:Empty method docstring:INFERENCE +empty-docstring:62:DDDD.method2:Empty method docstring:INFERENCE +missing-docstring:70:DDDD.method4:Missing method docstring:INFERENCE diff --git a/test/functional/invalid_exceptions_caught.txt b/test/functional/invalid_exceptions_caught.txt index 3156a0b..ce9ab2a 100644 --- a/test/functional/invalid_exceptions_caught.txt +++ b/test/functional/invalid_exceptions_caught.txt @@ -1,6 +1,6 @@ catching-non-exception:25::"Catching an exception which doesn't inherit from BaseException: MyException" catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MyException" catching-non-exception:31::"Catching an exception which doesn't inherit from BaseException: MySecondException" -catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: None -catching-non-exception:52::Catching an exception which doesn't inherit from BaseException: list() -catching-non-exception:57::Catching an exception which doesn't inherit from BaseException: None +catching-non-exception:52::"Catching an exception which doesn't inherit from BaseException: None" +catching-non-exception:52::"Catching an exception which doesn't inherit from BaseException: list()" +catching-non-exception:57::"Catching an exception which doesn't inherit from BaseException: None" diff --git a/test/functional/invalid_exceptions_raised.txt b/test/functional/invalid_exceptions_raised.txt index c40eca7..54ea0f4 100644 --- a/test/functional/invalid_exceptions_raised.txt +++ b/test/functional/invalid_exceptions_raised.txt @@ -1,13 +1,9 @@ -nonstandard-exception:23:bad_case0:"Exception doesn't inherit from standard ""Exception"" class" -raising-non-exception:23:bad_case0:"""Raising a new style class which doesn't inherit from BaseException -"" -" +nonstandard-exception:23:bad_case0:"Exception doesn't inherit from standard ""Exception"" class":INFERENCE +raising-non-exception:23:bad_case0:Raising a new style class which doesn't inherit from BaseException raising-non-exception:27:bad_case1:Raising a new style class which doesn't inherit from BaseException -nonstandard-exception:33:bad_case2:"Exception doesn't inherit from standard ""Exception"" class" +nonstandard-exception:33:bad_case2:"Exception doesn't inherit from standard ""Exception"" class":INFERENCE old-raise-syntax:33:bad_case2:Use raise ErrorClass(args) instead of raise ErrorClass, args. -raising-non-exception:33:bad_case2:"""Raising a new style class which doesn't inherit from BaseException -"" -" +raising-non-exception:33:bad_case2:Raising a new style class which doesn't inherit from BaseException raising-non-exception:37:bad_case3:Raising a new style class which doesn't inherit from BaseException notimplemented-raised:42:bad_case4:NotImplemented raised - should raise NotImplementedError old-raise-syntax:42:bad_case4:Use raise ErrorClass(args) instead of raise ErrorClass, args. diff --git a/test/functional/member_checks.py b/test/functional/member_checks.py new file mode 100644 index 0000000..fccf160 --- /dev/null +++ b/test/functional/member_checks.py @@ -0,0 +1,63 @@ +# pylint: disable= +"""check getattr if inference succeed""" + + +class Provider(object): + """provide some attributes and method""" + cattr = 4 + def __init__(self): + self.attr = 4 + def method(self, val): + """impressive method""" + return self.attr * val + def hophop(self): + """hop method""" + print 'hop hop hop', self + + +class Client(object): + """use provider class""" + + def __init__(self): + self._prov = Provider() + self._prov_attr = Provider.cattr + self._prov_attr2 = Provider.cattribute # [no-member] + self.set_later = 0 + + def set_set_later(self, value): + """set set_later attribute (introduce an inference ambiguity)""" + self.set_later = value + + def use_method(self): + """use provider's method""" + self._prov.hophop() + self._prov.hophophop() # [no-member] + + def use_attr(self): + """use provider's attr""" + print self._prov.attr + print self._prov.attribute # [no-member] + + def debug(self): + """print debug information""" + print self.__class__.__name__ + print self.__doc__ + print self.__dict__ + print self.__module__ + + def test_bt_types(self): + """test access to unexistant member of builtin types""" + lis = [] + lis.apppend(self) # [no-member] + dic = {} + dic.set(self) # [no-member] + tup = () + tup.append(self) # [no-member] + string = 'toto' + print string.loower() # [no-member] + integer = 1 + print integer.whatever # [no-member] + +print object.__init__ +print property.__init__ +print Client().set_later.lower() # [no-member] diff --git a/test/functional/member_checks.txt b/test/functional/member_checks.txt new file mode 100644 index 0000000..12fe6ee --- /dev/null +++ b/test/functional/member_checks.txt @@ -0,0 +1,9 @@ +no-member:24:Client.__init__:Class 'Provider' has no 'cattribute' member:INFERENCE +no-member:34:Client.use_method:Instance of 'Provider' has no 'hophophop' member:INFERENCE +no-member:39:Client.use_attr:Instance of 'Provider' has no 'attribute' member:INFERENCE +no-member:51:Client.test_bt_types:Instance of 'list' has no 'apppend' member:INFERENCE +no-member:53:Client.test_bt_types:Instance of 'dict' has no 'set' member:INFERENCE +no-member:55:Client.test_bt_types:Instance of 'tuple' has no 'append' member:INFERENCE +no-member:57:Client.test_bt_types:Instance of 'str' has no 'loower' member:INFERENCE +no-member:59:Client.test_bt_types:Instance of 'int' has no 'whatever' member:INFERENCE +no-member:63::Instance of 'int' has no 'lower' member:INFERENCE_FAILURE diff --git a/test/functional/name_styles.txt b/test/functional/name_styles.txt index 985e9a2..e81d27a 100644 --- a/test/functional/name_styles.txt +++ b/test/functional/name_styles.txt @@ -6,12 +6,12 @@ invalid-name:25:bad_class_name:"Invalid class name ""bad_class_name""" invalid-name:36:CorrectClassName.__init__:"Invalid attribute name ""_Bad_AtTR_name""" invalid-name:37:CorrectClassName.__init__:"Invalid attribute name ""Bad_PUBLIC_name""" invalid-name:39:CorrectClassName:"Invalid class attribute name ""zz""" -invalid-name:42:CorrectClassName.BadMethodName:"Invalid method name ""BadMethodName""" -invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:"Invalid method name ""__DunDER_IS_not_free_for_all__""" +invalid-name:42:CorrectClassName.BadMethodName:"Invalid method name ""BadMethodName""":INFERENCE +invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:"Invalid method name ""__DunDER_IS_not_free_for_all__""":INFERENCE invalid-name:78::"Invalid class name ""BAD_NAME_FOR_CLASS""" invalid-name:79::"Invalid class name ""NEXT_BAD_NAME_FOR_CLASS""" invalid-name:86::"Invalid class name ""NOT_CORRECT""" invalid-name:92:test_globals:"Invalid constant name ""AlsoCorrect""" -invalid-name:105:FooClass.PROPERTY_NAME:"Invalid attribute name ""PROPERTY_NAME""" -invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:"Invalid attribute name ""ABSTRACT_PROPERTY_NAME""" -invalid-name:115:FooClass.PROPERTY_NAME_SETTER:"Invalid attribute name ""PROPERTY_NAME_SETTER""" +invalid-name:105:FooClass.PROPERTY_NAME:"Invalid attribute name ""PROPERTY_NAME""":INFERENCE +invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:"Invalid attribute name ""ABSTRACT_PROPERTY_NAME""":INFERENCE +invalid-name:115:FooClass.PROPERTY_NAME_SETTER:"Invalid attribute name ""PROPERTY_NAME_SETTER""":INFERENCE diff --git a/test/functional/namedtuple_member_inference.txt b/test/functional/namedtuple_member_inference.txt index 87d9da4..308336f 100644 --- a/test/functional/namedtuple_member_inference.txt +++ b/test/functional/namedtuple_member_inference.txt @@ -1,3 +1,3 @@ -no-member:15:test:Class 'Thing' has no 'x' member +no-member:15:test:Class 'Thing' has no 'x' member:INFERENCE protected-access:19:test:Access to a protected member _replace of a client class -no-member:21:test:Instance of 'Fantastic' has no 'foo' member +no-member:21:test:Instance of 'Fantastic' has no 'foo' member:INFERENCE diff --git a/test/functional/newstyle__slots__.txt b/test/functional/newstyle__slots__.txt index afc9117..4320390 100644 --- a/test/functional/newstyle__slots__.txt +++ b/test/functional/newstyle__slots__.txt @@ -1,2 +1,2 @@ old-style-class:10:OldStyleClass:Old-style class defined. -slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class +slots-on-old-class:10:OldStyleClass:Use of __slots__ on an old style class:INFERENCE diff --git a/test/functional/newstyle_properties.txt b/test/functional/newstyle_properties.txt index 9f060c0..a16686b 100644 --- a/test/functional/newstyle_properties.txt +++ b/test/functional/newstyle_properties.txt @@ -1,2 +1,2 @@ old-style-class:13:OldStyleClass:Old-style class defined. -property-on-old-class:15:OldStyleClass:"Use of ""property"" on an old style class" +property-on-old-class:15:OldStyleClass:"Use of ""property"" on an old style class":INFERENCE diff --git a/test/functional/super_checks.txt b/test/functional/super_checks.txt index 2d69098..79f6ae1 100644 --- a/test/functional/super_checks.txt +++ b/test/functional/super_checks.txt @@ -1,8 +1,8 @@ old-style-class:6:Aaaa:Old-style class defined. -super-on-old-class:8:Aaaa.hop:Use of super on an old style class -super-on-old-class:12:Aaaa.__init__:Use of super on an old style class -bad-super-call:22:NewAaaa.__init__:Bad first argument 'object' given to super() -missing-super-argument:27:Py3kAaaa.__init__:Missing argument to super() -bad-super-call:32:Py3kWrongSuper.__init__:Bad first argument 'NewAaaa' given to super() -bad-super-call:37:WrongNameRegression.__init__:Bad first argument 'Missing' given to super() -bad-super-call:46:CrashSuper.__init__:Bad first argument 'NewAaaa' given to super() +super-on-old-class:8:Aaaa.hop:Use of super on an old style class:INFERENCE +super-on-old-class:12:Aaaa.__init__:Use of super on an old style class:INFERENCE +bad-super-call:22:NewAaaa.__init__:Bad first argument 'object' given to super():INFERENCE +missing-super-argument:27:Py3kAaaa.__init__:Missing argument to super():INFERENCE +bad-super-call:32:Py3kWrongSuper.__init__:Bad first argument 'NewAaaa' given to super():INFERENCE +bad-super-call:37:WrongNameRegression.__init__:Bad first argument 'Missing' given to super():INFERENCE +bad-super-call:46:CrashSuper.__init__:Bad first argument 'NewAaaa' given to super():INFERENCE diff --git a/test/input/func_typecheck_getattr.py b/test/input/func_typecheck_getattr.py deleted file mode 100644 index b120ca4..0000000 --- a/test/input/func_typecheck_getattr.py +++ /dev/null @@ -1,69 +0,0 @@ -# pylint: disable= -"""check getattr if inference succeed""" - -__revision__ = None - -class Provider(object): - """provide some attributes and method""" - cattr = 4 - def __init__(self): - self.attr = 4 - def method(self, val): - """impressive method""" - return self.attr * val - def hophop(self): - """hop method""" - print 'hop hop hop', self - - -class Client: - """use provider class""" - - def __init__(self): - self._prov = Provider() - self._prov_attr = Provider.cattr - self._prov_attr2 = Provider.cattribute - self.set_later = 0 - - def set_set_later(self, value): - """set set_later attribute (introduce an inference ambiguity)""" - self.set_later = value - - def use_method(self): - """use provider's method""" - self._prov.hophop() - self._prov.hophophop() - - def use_attr(self): - """use provider's attr""" - print self._prov.attr - print self._prov.attribute - - def debug(self): - """print debug information""" - print self.__class__.__name__ - print self.__doc__ - print self.__dict__ - print self.__module__ - - def test_bt_types(self): - """test access to unexistant member of builtin types""" - lis = [] - lis.apppend(self) - dic = {} - dic.set(self) - tup = () - tup.append(self) - string = 'toto' - print string.loower() - # unicode : moved to func_3k_removed_stuff_py_30.py - # - integer = 1 - print integer.whatever - -print object.__init__ -print property.__init__ -print Client().set_later.lower() - -# should detect mixing new style / old style classes -Client.__bases__ += (object,) diff --git a/test/messages/func_typecheck_getattr.txt b/test/messages/func_typecheck_getattr.txt deleted file mode 100644 index bd91228..0000000 --- a/test/messages/func_typecheck_getattr.txt +++ /dev/null @@ -1,10 +0,0 @@ -C: 19:Client: Old-style class defined. -E: 25:Client.__init__: Class 'Provider' has no 'cattribute' member -E: 35:Client.use_method: Instance of 'Provider' has no 'hophophop' member -E: 40:Client.use_attr: Instance of 'Provider' has no 'attribute' member -E: 52:Client.test_bt_types: Instance of 'list' has no 'apppend' member -E: 54:Client.test_bt_types: Instance of 'dict' has no 'set' member -E: 56:Client.test_bt_types: Instance of 'tuple' has no 'append' member -E: 58:Client.test_bt_types: Instance of 'str' has no 'loower' member -E: 62:Client.test_bt_types: Instance of 'int' has no 'whatever' member -E: 66: Instance of 'int' has no 'lower' member (but some types could not be inferred) diff --git a/test/test_functional.py b/test/test_functional.py index 759a447..c2467b3 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -11,6 +11,7 @@ import sys import unittest from pylint import checkers +from pylint import interfaces from pylint import lint from pylint import reporters from pylint import utils @@ -30,6 +31,10 @@ csv.register_dialect('test', test_dialect) class NoFileError(Exception): pass +# Notes: +# - for the purpose of this test, the confidence levels HIGH and UNDEFINED +# are treated as the same. + # TODOs # - implement exhaustivity tests @@ -37,14 +42,24 @@ class NoFileError(Exception): UPDATE = False class OutputLine(collections.namedtuple('OutputLine', - ['symbol', 'lineno', 'object', 'msg'])): + ['symbol', 'lineno', 'object', 'msg', 'confidence'])): @classmethod def from_msg(cls, msg): - return cls(msg.symbol, msg.line, msg.obj or '', msg.msg) + return cls( + msg.symbol, msg.line, msg.obj or '', msg.msg, + msg.confidence.name + if msg.confidence != interfaces.UNDEFINED else interfaces.HIGH.name) @classmethod def from_csv(cls, row): - return cls(row[0], int(row[1]), row[2], row[3]) + confidence = row[4] if len(row) == 5 else interfaces.HIGH.name + return cls(row[0], int(row[1]), row[2], row[3], confidence) + + def to_csv(self): + if self.confidence == interfaces.HIGH.name: + return self[:-1] + else: + return self # Common sub-expressions. @@ -292,7 +307,7 @@ class LintModuleOutputUpdate(LintModuleTest): try: return super(LintModuleOutputUpdate, self)._open_expected_file() except IOError: - return contextlib.closing(cStringIO.StringIO()) + return io.StringIO() def _check_output_text(self, expected_messages, expected_lines, received_lines): @@ -305,8 +320,7 @@ class LintModuleOutputUpdate(LintModuleTest): with open(self._test_file.expected_output, 'w') as fobj: writer = csv.writer(fobj, dialect='test') for line in remaining: - #fobj.write('{0}:{1}:{2}:{3}'.format(*line)) - writer.writerow(line) + writer.writerow(line.to_csv()) def suite(): input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), diff --git a/test/unittest_lint.py b/test/unittest_lint.py index b2022cc..7dc7090 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -26,12 +26,13 @@ from logilab.common.compat import reload from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError -from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ +from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, MSG_STATE_CONFIDENCE, \ MessagesStore, PyLintASTWalker, MessageDefinition, FileState, \ build_message_def, tokenize_module from pylint.testutils import TestReporter from pylint.reporters import text from pylint import checkers +from pylint import interfaces if sys.platform == 'win32': HOME = 'USERPROFILE' @@ -139,19 +140,26 @@ class PyLinterTC(unittest.TestCase): self.assertTrue(linter.is_message_enabled('C0121', line=1)) def test_message_state_scope(self): + class FakeConfig(object): + confidence = ['HIGH'] + linter = self.init_linter() - fs = linter.file_state linter.disable('C0121') self.assertEqual(MSG_STATE_SCOPE_CONFIG, - fs._message_state_scope('C0121')) + linter.get_message_state_scope('C0121')) linter.disable('W0101', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_CONFIG, - fs._message_state_scope('C0121')) + linter.get_message_state_scope('C0121')) self.assertEqual(MSG_STATE_SCOPE_MODULE, - fs._message_state_scope('W0101', 3)) + linter.get_message_state_scope('W0101', 3)) linter.enable('W0102', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_MODULE, - fs._message_state_scope('W0102', 3)) + linter.get_message_state_scope('W0102', 3)) + linter.config = FakeConfig() + self.assertEqual( + MSG_STATE_CONFIDENCE, + linter.get_message_state_scope('this-is-bad', + confidence=interfaces.INFERENCE)) def test_enable_message_block(self): linter = self.init_linter() diff --git a/test/unittest_utils.py b/test/unittest_utils.py index ef0cda2..d631dbb 100644 --- a/test/unittest_utils.py +++ b/test/unittest_utils.py @@ -15,6 +15,7 @@ import unittest from astroid import test_utils from pylint import utils +from pylint import interfaces from pylint.checkers.utils import check_messages @@ -59,4 +60,3 @@ class PyLintASTWalkerTest(unittest.TestCase): if __name__ == '__main__': unittest.main() - diff --git a/testutils.py b/testutils.py index e53ec70..86539ac 100644 --- a/testutils.py +++ b/testutils.py @@ -141,7 +141,7 @@ class UnittestLinter(object): finally: self._messages = [] - def add_message(self, msg_id, line=None, node=None, args=None): + def add_message(self, msg_id, line=None, node=None, args=None, confidence=None): self._messages.append(Message(msg_id, line, node, args)) def is_message_enabled(self, *unused_args): diff --git a/utils.py b/utils.py index 0d2cd3c..4e65a56 100644 --- a/utils.py +++ b/utils.py @@ -35,7 +35,7 @@ from astroid import nodes, Module from astroid.modutils import modpath_from_file, get_module_files, \ file_from_modpath, load_module_from_file -from pylint.interfaces import IRawChecker, ITokenChecker +from pylint.interfaces import IRawChecker, ITokenChecker, UNDEFINED class UnknownMessage(Exception): @@ -67,6 +67,7 @@ MSG_TYPES_STATUS = { _MSG_ORDER = 'EWRCIF' MSG_STATE_SCOPE_CONFIG = 0 MSG_STATE_SCOPE_MODULE = 1 +MSG_STATE_CONFIDENCE = 2 OPTION_RGX = re.compile(r'\s*#.*\bpylint:(.*)') @@ -79,15 +80,16 @@ class WarningScope(object): _MsgBase = collections.namedtuple( '_MsgBase', - ['msg_id', 'symbol', 'msg', 'C', 'category', 'abspath', 'module', 'obj', - 'line', 'column']) + ['msg_id', 'symbol', 'msg', 'C', 'category', 'confidence', + 'abspath', 'module', 'obj', 'line', 'column']) class Message(_MsgBase): """This class represent a message to be issued by the reporters""" - def __new__(cls, msg_id, symbol, location, msg): + def __new__(cls, msg_id, symbol, location, msg, confidence): return _MsgBase.__new__( - cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], *location) + cls, msg_id, symbol, msg, msg_id[0], MSG_TYPES[msg_id[0]], + confidence, *location) def format(self, template): """Format the message according to the given template. @@ -302,12 +304,25 @@ class MessagesHandlerMixIn(object): # sync configuration object self.config.enable = [mid for mid, val in msgs.iteritems() if val] - def is_message_enabled(self, msg_descr, line=None): + def get_message_state_scope(self, msgid, line=None, confidence=UNDEFINED): + """Returns the scope at which a message was enabled/disabled.""" + if self.config.confidence and confidence.name not in self.config.confidence: + return MSG_STATE_CONFIDENCE + try: + if line in self.file_state._module_msgs_state[msgid]: + return MSG_STATE_SCOPE_MODULE + except (KeyError, TypeError): + return MSG_STATE_SCOPE_CONFIG + + def is_message_enabled(self, msg_descr, line=None, confidence=None): """return true if the message associated to the given message id is enabled msgid may be either a numeric or symbolic message id. """ + if self.config.confidence and confidence: + if confidence.name not in self.config.confidence: + return False try: msgid = self.msgs_store.check_message_id(msg_descr).msgid except UnknownMessage: @@ -322,7 +337,7 @@ class MessagesHandlerMixIn(object): except KeyError: return self._msgs_state.get(msgid, True) - def add_message(self, msg_descr, line=None, node=None, args=None): + def add_message(self, msg_descr, line=None, node=None, args=None, confidence=UNDEFINED): """Adds a message given by ID or name. If provided, the message string is expanded using args @@ -352,8 +367,10 @@ class MessagesHandlerMixIn(object): else: col_offset = None # should this message be displayed - if not self.is_message_enabled(msgid, line): - self.file_state.handle_ignored_message(msgid, line, node, args) + if not self.is_message_enabled(msgid, line, confidence): + self.file_state.handle_ignored_message( + self.get_message_state_scope(msgid, line, confidence), + msgid, line, node, args, confidence) return # update stats msg_cat = MSG_TYPES[msgid[0]] @@ -377,8 +394,8 @@ class MessagesHandlerMixIn(object): path = node.root().file # add the message self.reporter.handle_message( - Message(msgid, symbol, - (path, module, obj, line or 1, col_offset or 0), msg)) + Message(msgid, symbol, + (path, module, obj, line or 1, col_offset or 0), msg, confidence)) def print_full_documentation(self): """output a full documentation in ReST format""" @@ -519,14 +536,13 @@ class FileState(object): except KeyError: self._module_msgs_state[msg.msgid] = {line: status} - def handle_ignored_message(self, msgid, line, node, args): + def handle_ignored_message(self, state_scope, msgid, line, node, args, confidence): """Report an ignored message. state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, depending on whether the message was disabled locally in the module, or globally. The other arguments are the same as for add_message. """ - state_scope = self._message_state_scope(msgid, line) if state_scope == MSG_STATE_SCOPE_MODULE: try: orig_line = self._suppression_mapping[(msgid, line)] @@ -534,14 +550,6 @@ class FileState(object): except KeyError: pass - def _message_state_scope(self, msgid, line=None): - """Returns the scope at which a message was enabled/disabled.""" - try: - if line in self._module_msgs_state[msgid]: - return MSG_STATE_SCOPE_MODULE - except KeyError: - return MSG_STATE_SCOPE_CONFIG - def iter_spurious_suppression_messages(self, msgs_store): for warning, lines in self._raw_module_msgs_state.iteritems(): for line, enable in lines.iteritems(): -- cgit v1.2.1 From 567fb4030d5ab12ccaf5d19f5cfbf35c44bc95de Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Thu, 24 Jul 2014 18:20:21 +0200 Subject: fix bad indentation issues --- test/unittest_lint.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/unittest_lint.py b/test/unittest_lint.py index 7dd403b..a856d73 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -33,9 +33,9 @@ from pylint.reporters import text from pylint import checkers if sys.platform == 'win32': - HOME = 'USERPROFILE' + HOME = 'USERPROFILE' else: - HOME = 'HOME' + HOME = 'HOME' class GetNoteMessageTC(TestCase): def test(self): @@ -381,10 +381,10 @@ class PyLinterTC(TestCase): 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-hook', 'raise RuntimeError']) - self.assertRaises(RuntimeError, - Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant']) + self.assertRaises(RuntimeError, + Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError']) + self.assertRaises(RuntimeError, + Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant']) def test_analyze_explicit_script(self): @@ -394,6 +394,7 @@ class PyLinterTC(TestCase): ['C: 2: Line too long (175/80)'], self.linter.reporter.messages) + class ConfigTC(TestCase): def setUp(self): -- cgit v1.2.1 From 3b44ac5a6f38449e50153768f10fd1c1985608a0 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 25 Jul 2014 10:42:41 +0200 Subject: Fix more edge cases in the cell-var-from-loop warning (Closes: #233). --- checkers/variables.py | 10 +++++++++- test/input/func_loopvar_in_closure.py | 18 ++++++++++++++++++ test/messages/func_loopvar_in_closure.txt | 12 ++++++------ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/checkers/variables.py b/checkers/variables.py index 8f8ee87..3b9bcda 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -542,9 +542,15 @@ builtins. Remember that you should avoid to define new builtins when possible.' self.add_message('global-statement', node=node) def _check_late_binding_closure(self, node, assignment_node, scope_type): + def _is_direct_lambda_call(): + return (isinstance(node_scope.parent, astroid.CallFunc) + and node_scope.parent.func is node_scope) + node_scope = node.scope() if not isinstance(node_scope, (astroid.Lambda, astroid.Function)): return + if isinstance(node.parent, astroid.Arguments): + return if isinstance(assignment_node, astroid.Comprehension): if assignment_node.parent.parent_of(node.scope()): @@ -557,7 +563,9 @@ builtins. Remember that you should avoid to define new builtins when possible.' break maybe_for = maybe_for.parent else: - if maybe_for.parent_of(node_scope) and not isinstance(node_scope.statement(), astroid.Return): + if (maybe_for.parent_of(node_scope) + and not _is_direct_lambda_call() + and not isinstance(node_scope.statement(), astroid.Return)): self.add_message('cell-var-from-loop', node=node, args=node.name) def _loopvar_name(self, node, name): diff --git a/test/input/func_loopvar_in_closure.py b/test/input/func_loopvar_in_closure.py index 32b7a6c..3a791d3 100644 --- a/test/input/func_loopvar_in_closure.py +++ b/test/input/func_loopvar_in_closure.py @@ -53,6 +53,23 @@ def good_case7(): return lambda: -1 +def good_case8(): + """Lambda defined and called in loop.""" + for i in range(10): + print (lambda x: i + x)(1) + + +def good_case9(): + """Another eager binding of the cell variable.""" + funs = [] + for i in range(10): + def func(bound_i=i): + """Ignore.""" + return bound_i + funs.append(func) + return funs + + def bad_case(): """Closing over a loop variable.""" lst = [] @@ -112,3 +129,4 @@ def bad_case6(): print j lst.append(lambda: i) return lst + diff --git a/test/messages/func_loopvar_in_closure.txt b/test/messages/func_loopvar_in_closure.txt index 6ca613a..5b068f4 100644 --- a/test/messages/func_loopvar_in_closure.txt +++ b/test/messages/func_loopvar_in_closure.txt @@ -1,8 +1,8 @@ W: 21:good_case3: Unused variable 'i' W: 45:good_case6.: Using possibly undefined loop variable 'i' -W: 61:bad_case.: Cell variable i defined in loop -W: 66:bad_case2.: Cell variable i defined in loop -W: 74:bad_case3.: Cell variable j defined in loop -W: 84:bad_case4.nested: Cell variable i defined in loop -W:105:bad_case5.: Cell variable i defined in loop -W:113:bad_case6.: Cell variable i defined in loop +W: 78:bad_case.: Cell variable i defined in loop +W: 83:bad_case2.: Cell variable i defined in loop +W: 91:bad_case3.: Cell variable j defined in loop +W:101:bad_case4.nested: Cell variable i defined in loop +W:122:bad_case5.: Cell variable i defined in loop +W:130:bad_case6.: Cell variable i defined in loop -- cgit v1.2.1 From 328c824b73debaeee050f39043ba78b0b512ecc5 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 25 Jul 2014 10:11:48 +0200 Subject: Allow implicitly concatenated strings to align with the previous string part. --- checkers/format.py | 35 ++++++++++++++++++++--------------- test/input/func_bad_continuation.py | 4 ++++ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/checkers/format.py b/checkers/format.py index 8b73049..c07ff32 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -336,16 +336,18 @@ class ContinuedLineState(object): _BeforeBlockOffsets(indentation + self._continuation_size, indentation + self._continuation_size * 2)) elif bracket == ':': - if self._cont_stack[-1].context_type == CONTINUED: - # If the dict key was on the same line as the open brace, the new - # correct indent should be relative to the key instead of the - # current indent level - paren_align = self._cont_stack[-1].valid_outdent_offsets - next_align = self._cont_stack[-1].valid_continuation_offsets.copy() - next_align[next_align.keys()[0] + self._continuation_size] = True - else: - next_align = _Offsets(indentation + self._continuation_size, indentation) - paren_align = _Offsets(indentation + self._continuation_size, indentation) + # If the dict key was on the same line as the open brace, the new + # correct indent should be relative to the key instead of the + # current indent level + paren_align = self._cont_stack[-1].valid_outdent_offsets + next_align = self._cont_stack[-1].valid_continuation_offsets.copy() + next_align[next_align.keys()[0] + self._continuation_size] = True + # Note that the continuation of + # d = { + # 'a': 'b' + # 'c' + # } + # is handled by the special-casing for hanging continued string indents. return _ContinuedIndent(HANGING_DICT_VALUE, bracket, position, paren_align, next_align) else: return _ContinuedIndent( @@ -793,16 +795,19 @@ class FormatChecker(BaseTokenChecker): self._add_continuation_message(state, hints, tokens, indent_pos) def _check_continued_indentation(self, tokens, next_idx): + def same_token_around_nl(token_type): + return (tokens.type(next_idx) == token_type and + tokens.type(next_idx-2) == token_type) + # Do not issue any warnings if the next line is empty. if not self._current_line.has_content or tokens.type(next_idx) == tokenize.NL: return state, valid_offsets = self._current_line.get_valid_offsets(next_idx) - # Special handling for hanging comments. If the last line ended with a - # comment and the new line contains only a comment, the line may also be - # indented to the start of the previous comment. - if (tokens.type(next_idx) == tokenize.COMMENT and - tokens.type(next_idx-2) == tokenize.COMMENT): + # Special handling for hanging comments and strings. If the last line ended + # with a comment (string) and the new line contains only a comment, the line + # may also be indented to the start of the previous token. + if same_token_around_nl(tokenize.COMMENT) or same_token_around_nl(tokenize.STRING): valid_offsets[tokens.start_col(next_idx-2)] = True # We can only decide if the indentation of a continued line before opening diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py index 03beaf4..f303915 100644 --- a/test/input/func_bad_continuation.py +++ b/test/input/func_bad_continuation.py @@ -185,3 +185,7 @@ if not (1 and 2): # [bad-continuation] print 3 +continue2("foo", + some_other_arg="this " + "is " + "fine") -- cgit v1.2.1 From d76865583f0103915150ed901afeceba9f2eadba Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Thu, 24 Jul 2014 19:04:47 +0200 Subject: Make pylint compatible with Python 2.5 again, and make all the tests pass (Closes: #278). --- checkers/typecheck.py | 3 +- test/functional/abstract_abc_methods.args | 3 + test/functional/abstract_abc_methods.py | 17 +++ test/functional/anomalous_unicode_escape.args | 2 + test/functional/anomalous_unicode_escape.py | 20 +++ test/functional/anomalous_unicode_escape.txt | 3 + test/functional/future_unicode_literals.args | 2 + test/functional/future_unicode_literals.py | 6 + test/functional/future_unicode_literals.txt | 1 + test/functional/name_styles.args | 5 + test/functional/name_styles.py | 117 ++++++++++++++++++ test/functional/name_styles.txt | 17 +++ test/functional/namedtuple_member_inference.args | 2 + test/functional/namedtuple_member_inference.py | 21 ++++ test/functional/namedtuple_member_inference.txt | 3 + test/functional/suspicious_str_strip_call.args | 3 + test/functional/suspicious_str_strip_call.py | 9 ++ test/functional/suspicious_str_strip_call.txt | 3 + test/functional/suspicious_str_strip_call_py3.args | 2 + test/functional/suspicious_str_strip_call_py3.py | 9 ++ test/functional/suspicious_str_strip_call_py3.txt | 3 + test/input/func_bad_str_strip_call.py | 9 -- test/input/func_ctor_arguments.py | 7 +- test/input/func_name_checking.py | 135 --------------------- test/input/func_namedtuple.py | 10 -- test/input/func_noerror_abstract_method.py | 20 --- test/input/func_noerror_abstract_method_py30.py | 19 --- test/input/func_raw_escapes.py | 19 --- test/input/func_unicode_literal.py | 7 -- test/input/func_w0404.py | 6 +- test/messages/func_bad_str_strip_call.txt | 3 - test/messages/func_bad_str_strip_call_py30.txt | 3 - test/messages/func_name_checking.txt | 18 --- test/messages/func_namedtuple.txt | 1 - test/messages/func_w0404.txt | 2 +- test/test_func.py | 2 +- test/test_functional.py | 9 +- test/unittest_checker_base.py | 4 + test/unittest_checker_classes.py | 1 + test/unittest_checker_format.py | 1 + test/unittest_checker_logging.py | 1 + test/unittest_checker_misc.py | 2 + test/unittest_checker_typecheck.py | 1 + test/unittest_checker_variables.py | 1 + test/unittest_checkers_utils.py | 1 + testutils.py | 28 ++++- 46 files changed, 301 insertions(+), 260 deletions(-) create mode 100644 test/functional/abstract_abc_methods.args create mode 100644 test/functional/abstract_abc_methods.py create mode 100644 test/functional/anomalous_unicode_escape.args create mode 100644 test/functional/anomalous_unicode_escape.py create mode 100644 test/functional/anomalous_unicode_escape.txt create mode 100644 test/functional/future_unicode_literals.args create mode 100644 test/functional/future_unicode_literals.py create mode 100644 test/functional/future_unicode_literals.txt create mode 100644 test/functional/name_styles.args create mode 100644 test/functional/name_styles.py create mode 100644 test/functional/name_styles.txt create mode 100644 test/functional/namedtuple_member_inference.args create mode 100644 test/functional/namedtuple_member_inference.py create mode 100644 test/functional/namedtuple_member_inference.txt create mode 100644 test/functional/suspicious_str_strip_call.args create mode 100644 test/functional/suspicious_str_strip_call.py create mode 100644 test/functional/suspicious_str_strip_call.txt create mode 100644 test/functional/suspicious_str_strip_call_py3.args create mode 100644 test/functional/suspicious_str_strip_call_py3.py create mode 100644 test/functional/suspicious_str_strip_call_py3.txt delete mode 100644 test/input/func_bad_str_strip_call.py delete mode 100644 test/input/func_name_checking.py delete mode 100644 test/input/func_namedtuple.py delete mode 100644 test/input/func_noerror_abstract_method.py delete mode 100644 test/input/func_noerror_abstract_method_py30.py delete mode 100644 test/input/func_raw_escapes.py delete mode 100644 test/input/func_unicode_literal.py delete mode 100644 test/messages/func_bad_str_strip_call.txt delete mode 100644 test/messages/func_bad_str_strip_call_py30.txt delete mode 100644 test/messages/func_name_checking.txt delete mode 100644 test/messages/func_namedtuple.txt diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 3bcb099..efc0909 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -386,7 +386,8 @@ accessed. Python regular expressions are accepted.'} if isinstance(arg, astroid.Keyword): keyword = arg.arg if keyword in keyword_args: - self.add_message('duplicate-keyword-arg', node=node, args=keyword) + self.add_message('duplicate-keyword-arg', node=node, + args=(keyword, 'function')) keyword_args.add(keyword) else: num_positional_args += 1 diff --git a/test/functional/abstract_abc_methods.args b/test/functional/abstract_abc_methods.args new file mode 100644 index 0000000..2b38575 --- /dev/null +++ b/test/functional/abstract_abc_methods.args @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=2.6 + diff --git a/test/functional/abstract_abc_methods.py b/test/functional/abstract_abc_methods.py new file mode 100644 index 0000000..2e2bb13 --- /dev/null +++ b/test/functional/abstract_abc_methods.py @@ -0,0 +1,17 @@ +""" This should not warn about `prop` being abstract in Child """ +# pylint: disable=too-few-public-methods,abstract-class-little-used + +import abc + +class Parent(object): + """Abstract Base 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/functional/anomalous_unicode_escape.args b/test/functional/anomalous_unicode_escape.args new file mode 100644 index 0000000..8bfa6c0 --- /dev/null +++ b/test/functional/anomalous_unicode_escape.args @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=2.6 diff --git a/test/functional/anomalous_unicode_escape.py b/test/functional/anomalous_unicode_escape.py new file mode 100644 index 0000000..19a6912 --- /dev/null +++ b/test/functional/anomalous_unicode_escape.py @@ -0,0 +1,20 @@ +# pylint:disable=W0105, W0511, C0121 +"""Test for backslash escapes in byte vs unicode strings""" + +# Would be valid in Unicode, but probably not what you want otherwise +BAD_UNICODE = b'\u0042' # [anomalous-unicode-escape-in-string] +BAD_LONG_UNICODE = b'\U00000042' # [anomalous-unicode-escape-in-string] +# +1:[anomalous-unicode-escape-in-string] +BAD_NAMED_UNICODE = b'\N{GREEK SMALL LETTER ALPHA}' + +GOOD_UNICODE = u'\u0042' +GOOD_LONG_UNICODE = u'\U00000042' +GOOD_NAMED_UNICODE = u'\N{GREEK SMALL LETTER ALPHA}' + + +# Valid raw strings +RAW_BACKSLASHES = r'raw' +RAW_UNICODE = ur"\u0062\n" + +# In a comment you can have whatever you want: \ \\ \n \m +# even things that look like bad strings: "C:\Program Files" diff --git a/test/functional/anomalous_unicode_escape.txt b/test/functional/anomalous_unicode_escape.txt new file mode 100644 index 0000000..5c1acc7 --- /dev/null +++ b/test/functional/anomalous_unicode_escape.txt @@ -0,0 +1,3 @@ +anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. +anomalous-unicode-escape-in-string:6::Anomalous Unicode escape in byte string: '\U'. String constant might be missing an r or u prefix. +anomalous-unicode-escape-in-string:8::Anomalous Unicode escape in byte string: '\N'. String constant might be missing an r or u prefix. diff --git a/test/functional/future_unicode_literals.args b/test/functional/future_unicode_literals.args new file mode 100644 index 0000000..d5fd0ce --- /dev/null +++ b/test/functional/future_unicode_literals.args @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=2.6 \ No newline at end of file diff --git a/test/functional/future_unicode_literals.py b/test/functional/future_unicode_literals.py new file mode 100644 index 0000000..30c2bd6 --- /dev/null +++ b/test/functional/future_unicode_literals.py @@ -0,0 +1,6 @@ +"""Unicode literals in Python 2.*""" +from __future__ import unicode_literals + + +BAD_STRING = b'\u1234' # >= 2.7.4:[anomalous-unicode-escape-in-string] +GOOD_STRING = '\u1234' diff --git a/test/functional/future_unicode_literals.txt b/test/functional/future_unicode_literals.txt new file mode 100644 index 0000000..c490da5 --- /dev/null +++ b/test/functional/future_unicode_literals.txt @@ -0,0 +1 @@ +anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix. diff --git a/test/functional/name_styles.args b/test/functional/name_styles.args new file mode 100644 index 0000000..1a63e67 --- /dev/null +++ b/test/functional/name_styles.args @@ -0,0 +1,5 @@ +[testoptions] +min_pyver=2.6 + +[Messages Control] +disable=too-few-public-methods,abstract-class-not-used,global-statement diff --git a/test/functional/name_styles.py b/test/functional/name_styles.py new file mode 100644 index 0000000..be3e96d --- /dev/null +++ b/test/functional/name_styles.py @@ -0,0 +1,117 @@ +"""Test for the invalid-name warning.""" +import abc +import collections + +GOOD_CONST_NAME = '' +bad_const_name = 0 # [invalid-name] + + +def BADFUNCTION_name(): # [invalid-name] + """Bad function name.""" + BAD_LOCAL_VAR = 1 # [invalid-name] + print BAD_LOCAL_VAR + + +def func_bad_argname(NOT_GOOD): # [invalid-name] + """Function with a badly named argument.""" + return NOT_GOOD + + +def no_nested_args(arg1, arg21, arg22): + """Well-formed function.""" + print arg1, arg21, arg22 + + +class bad_class_name(object): # [invalid-name] + """Class with a bad name.""" + + +class CorrectClassName(object): + """Class with a good name.""" + + def __init__(self): + self._good_private_name = 10 + self.__good_real_private_name = 11 + self.good_attribute_name = 12 + self._Bad_AtTR_name = None # [invalid-name] + self.Bad_PUBLIC_name = None # [invalid-name] + + zz = 'Bad Class Attribute' # [invalid-name] + GOOD_CLASS_ATTR = 'Good Class Attribute' + + def BadMethodName(self): # [invalid-name] + """A Method with a bad name.""" + + def good_method_name(self): + """A method with a good name.""" + + def __DunDER_IS_not_free_for_all__(self): # [invalid-name] + """Another badly named method.""" + + +class DerivedFromCorrect(CorrectClassName): + """A derived class with an invalid inherited members. + + Derived attributes and methods with invalid names do not trigger warnings. + """ + zz = 'Now a good class attribute' + + def __init__(self): + super(DerivedFromCorrect, self).__init__() + self._Bad_AtTR_name = None # Ignored + + def BadMethodName(self): + """Ignored since the method is in the interface.""" + + +V = [WHAT_Ever_inListComp for WHAT_Ever_inListComp in GOOD_CONST_NAME] + +def class_builder(): + """Function returning a class object.""" + + class EmbeddedClass(object): + """Useless class.""" + + return EmbeddedClass + +# +1:[invalid-name] +BAD_NAME_FOR_CLASS = collections.namedtuple('Named', ['tuple']) +NEXT_BAD_NAME_FOR_CLASS = class_builder() # [invalid-name] + +GoodName = collections.namedtuple('Named', ['tuple']) +ToplevelClass = class_builder() + +# Aliases for classes have the same name constraints. +AlsoCorrect = CorrectClassName +NOT_CORRECT = CorrectClassName # [invalid-name] + + +def test_globals(): + """Names in global statements are also checked.""" + global NOT_CORRECT + global AlsoCorrect # [invalid-name] + NOT_CORRECT = 1 + AlsoCorrect = 2 + + +class FooClass(object): + """A test case for property names. + + Since by default, the regex for attributes is the same as the one + for method names, we check the warning messages to contain the + string 'attribute'. + """ + @property + def PROPERTY_NAME(self): # [invalid-name] + """Ignored.""" + pass + + @abc.abstractproperty + def ABSTRACT_PROPERTY_NAME(self): # [invalid-name] + """Ignored.""" + pass + + @PROPERTY_NAME.setter + def PROPERTY_NAME_SETTER(self): # [invalid-name] + """Ignored.""" + pass diff --git a/test/functional/name_styles.txt b/test/functional/name_styles.txt new file mode 100644 index 0000000..4f0ae53 --- /dev/null +++ b/test/functional/name_styles.txt @@ -0,0 +1,17 @@ +invalid-name:6::Invalid constant name "bad_const_name" +invalid-name:9:BADFUNCTION_name:Invalid function name "BADFUNCTION_name" +invalid-name:11:BADFUNCTION_name:Invalid variable name "BAD_LOCAL_VAR" +invalid-name:15:func_bad_argname:Invalid argument name "NOT_GOOD" +invalid-name:25:bad_class_name:Invalid class name "bad_class_name" +invalid-name:36:CorrectClassName.__init__:Invalid attribute name "_Bad_AtTR_name" +invalid-name:37:CorrectClassName.__init__:Invalid attribute name "Bad_PUBLIC_name" +invalid-name:39:CorrectClassName:Invalid class attribute name "zz" +invalid-name:42:CorrectClassName.BadMethodName:Invalid method name "BadMethodName" +invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:Invalid method name "__DunDER_IS_not_free_for_all__" +invalid-name:78::Invalid class name "BAD_NAME_FOR_CLASS" +invalid-name:79::Invalid class name "NEXT_BAD_NAME_FOR_CLASS" +invalid-name:86::Invalid class name "NOT_CORRECT" +invalid-name:92:test_globals:Invalid constant name "AlsoCorrect" +invalid-name:105:FooClass.PROPERTY_NAME:Invalid attribute name "PROPERTY_NAME" +invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:Invalid attribute name "ABSTRACT_PROPERTY_NAME" +invalid-name:115:FooClass.PROPERTY_NAME_SETTER:Invalid attribute name "PROPERTY_NAME_SETTER" diff --git a/test/functional/namedtuple_member_inference.args b/test/functional/namedtuple_member_inference.args new file mode 100644 index 0000000..8bfa6c0 --- /dev/null +++ b/test/functional/namedtuple_member_inference.args @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=2.6 diff --git a/test/functional/namedtuple_member_inference.py b/test/functional/namedtuple_member_inference.py new file mode 100644 index 0000000..675c3e2 --- /dev/null +++ b/test/functional/namedtuple_member_inference.py @@ -0,0 +1,21 @@ +"""Test namedtuple attributes. + +Regression test for: +https://bitbucket.org/logilab/pylint/issue/93/pylint-crashes-on-namedtuple-attribute +""" +__revision__ = None + +from collections import namedtuple +Thing = namedtuple('Thing', ()) + +Fantastic = namedtuple('Fantastic', ['foo']) + +def test(): + """Test member access in named tuples.""" + print Thing.x # [no-member] + fan = Fantastic(1) + print fan.foo + # Should not raise protected-access. + fan2 = fan._replace(foo=2) # [protected-access] + # This is a bug. + print fan2.foo # [no-member] diff --git a/test/functional/namedtuple_member_inference.txt b/test/functional/namedtuple_member_inference.txt new file mode 100644 index 0000000..87d9da4 --- /dev/null +++ b/test/functional/namedtuple_member_inference.txt @@ -0,0 +1,3 @@ +no-member:15:test:Class 'Thing' has no 'x' member +protected-access:19:test:Access to a protected member _replace of a client class +no-member:21:test:Instance of 'Fantastic' has no 'foo' member diff --git a/test/functional/suspicious_str_strip_call.args b/test/functional/suspicious_str_strip_call.args new file mode 100644 index 0000000..ecf5dcd --- /dev/null +++ b/test/functional/suspicious_str_strip_call.args @@ -0,0 +1,3 @@ +[testoptions] +min_pyver=2.6 +max_pyver=3.0 diff --git a/test/functional/suspicious_str_strip_call.py b/test/functional/suspicious_str_strip_call.py new file mode 100644 index 0000000..e859f25 --- /dev/null +++ b/test/functional/suspicious_str_strip_call.py @@ -0,0 +1,9 @@ +"""Suspicious str.strip calls.""" +__revision__ = 1 + +''.strip('yo') +''.strip() + +u''.strip('http://') # [bad-str-strip-call] +u''.lstrip('http://') # [bad-str-strip-call] +b''.rstrip('http://') # [bad-str-strip-call] diff --git a/test/functional/suspicious_str_strip_call.txt b/test/functional/suspicious_str_strip_call.txt new file mode 100644 index 0000000..ad714cc --- /dev/null +++ b/test/functional/suspicious_str_strip_call.txt @@ -0,0 +1,3 @@ +bad-str-strip-call:7::Suspicious argument in unicode.strip call +bad-str-strip-call:8::Suspicious argument in unicode.lstrip call +bad-str-strip-call:9::Suspicious argument in str.rstrip call diff --git a/test/functional/suspicious_str_strip_call_py3.args b/test/functional/suspicious_str_strip_call_py3.args new file mode 100644 index 0000000..c093be2 --- /dev/null +++ b/test/functional/suspicious_str_strip_call_py3.args @@ -0,0 +1,2 @@ +[testoptions] +min_pyver=3.0 diff --git a/test/functional/suspicious_str_strip_call_py3.py b/test/functional/suspicious_str_strip_call_py3.py new file mode 100644 index 0000000..e859f25 --- /dev/null +++ b/test/functional/suspicious_str_strip_call_py3.py @@ -0,0 +1,9 @@ +"""Suspicious str.strip calls.""" +__revision__ = 1 + +''.strip('yo') +''.strip() + +u''.strip('http://') # [bad-str-strip-call] +u''.lstrip('http://') # [bad-str-strip-call] +b''.rstrip('http://') # [bad-str-strip-call] diff --git a/test/functional/suspicious_str_strip_call_py3.txt b/test/functional/suspicious_str_strip_call_py3.txt new file mode 100644 index 0000000..81f32cf --- /dev/null +++ b/test/functional/suspicious_str_strip_call_py3.txt @@ -0,0 +1,3 @@ +bad-str-strip-call:7::Suspicious argument in str.strip call +bad-str-strip-call:8::Suspicious argument in str.lstrip call +bad-str-strip-call:9::Suspicious argument in bytes.rstrip call diff --git a/test/input/func_bad_str_strip_call.py b/test/input/func_bad_str_strip_call.py deleted file mode 100644 index 2d94a6e..0000000 --- a/test/input/func_bad_str_strip_call.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Suspicious str.strip calls.""" -__revision__ = 1 - -''.strip('yo') -''.strip() - -u''.strip('http://') -u''.lstrip('http://') -b''.rstrip('http://') diff --git a/test/input/func_ctor_arguments.py b/test/input/func_ctor_arguments.py index c708855..7bfc3a2 100644 --- a/test/input/func_ctor_arguments.py +++ b/test/input/func_ctor_arguments.py @@ -62,13 +62,16 @@ ClassNew(1, kwarg=1) ClassNew(1, 2, 3) ClassNew(one=2) -import abc + +class Metaclass(type): + pass + def with_metaclass(meta, base=object): """Create a new type that can be used as a metaclass.""" return meta("NewBase", (base, ), {}) -class ClassWithMeta(with_metaclass(abc.ABCMeta)): +class ClassWithMeta(with_metaclass(Metaclass)): pass ClassWithMeta() diff --git a/test/input/func_name_checking.py b/test/input/func_name_checking.py deleted file mode 100644 index d19d946..0000000 --- a/test/input/func_name_checking.py +++ /dev/null @@ -1,135 +0,0 @@ -# pylint: disable=R0903,R0201,R0921,W0603 -"""Test for the invalid-name (C0103) warning.""" - -__revision__ = 1 - -import collections - -def Run(): - """method without any good name""" - class B(object): - """nested class should not be tested has a variable""" - def __init__(self): - pass - bBb = 1 - return A, bBb, B - -def run(): - """anothrer method without only good name""" - class Aaa(object): - """nested class should not be tested has a variable""" - def __init__(self): - pass - bbb = 1 - return bbb, Aaa - -A = None - -def HOHOHOHO(): - """yo""" - HIHIHI = 1 - print HIHIHI - -class xyz(object): - """yo""" - - zz = 'Bad Class Attribute' - - def __init__(self): - pass - - def Youplapoum(self): - """bad method name""" - - -class Derived(xyz): - """Derived class.""" - zz = 'Not a bad class attribute' - - -def no_nested_args(arg1, arg21, arg22): - """a function which had nested arguments but no more""" - print arg1, arg21, arg22 - - -GOOD_CONST_NAME = '' -benpasceluila = 0 - -class Correct(object): - """yo""" - def __init__(self): - self.cava = 12 - self._Ca_va_Pas = None - - def BadMethodName(self): - """Ignored.""" - -V = [WHAT_Ever_inListComp for WHAT_Ever_inListComp in GOOD_CONST_NAME] - -def class_builder(): - """Function returning a class object.""" - - class EmbeddedClass(object): - """Useless class.""" - - return EmbeddedClass - -BAD_NAME_FOR_CLASS = collections.namedtuple('Named', ['tuple']) -NEXT_BAD_NAME_FOR_CLASS = class_builder() - -GoodName = collections.namedtuple('Named', ['tuple']) -ToplevelClass = class_builder() - -AlsoCorrect = Correct -NOT_CORRECT = Correct - - -def test_globals(): - """Names in global statements are also checked.""" - global NOT_CORRECT - global AlsoCorrect - NOT_CORRECT = 1 - AlsoCorrect = 2 - - -class DerivedFromCorrect(Correct): - """A derived class with an invalid inherited members. - - Derived attributes and methods with invalid names do not trigger warnings. - """ - - def __init__(self): - super(DerivedFromCorrect, self).__init__() - self._Ca_va_Pas = None - - def BadMethodName(self): - """Ignored.""" - -import abc - -class FooClass(object): - """A test case for property names. - - Since by default, the regex for attributes is the same as the one - for method names, we check the warning messages to contain the - string 'attribute'. - """ - @property - def PROPERTY_NAME(self): - """Ignored.""" - pass - - @abc.abstractproperty - def ABSTRACT_PROPERTY_NAME(self): - """Ignored.""" - pass - - @PROPERTY_NAME.setter - def PROPERTY_NAME_SETTER(self): - """Ignored.""" - pass - - -def func_bad_argname(NOT_GOOD): - """Function with a badly named argument.""" - return NOT_GOOD diff --git a/test/input/func_namedtuple.py b/test/input/func_namedtuple.py deleted file mode 100644 index 8cfd048..0000000 --- a/test/input/func_namedtuple.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Test namedtuple attributes. - -Regression test for: -https://bitbucket.org/logilab/pylint/issue/93/pylint-crashes-on-namedtuple-attribute -""" -__revision__ = None - -from collections import namedtuple -Thing = namedtuple('Thing', ()) -print Thing.x diff --git a/test/input/func_noerror_abstract_method.py b/test/input/func_noerror_abstract_method.py deleted file mode 100644 index 18228c6..0000000 --- a/test/input/func_noerror_abstract_method.py +++ /dev/null @@ -1,20 +0,0 @@ -""" 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 deleted file mode 100644 index c237cb4..0000000 --- a/test/input/func_noerror_abstract_method_py30.py +++ /dev/null @@ -1,19 +0,0 @@ -""" 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_raw_escapes.py b/test/input/func_raw_escapes.py deleted file mode 100644 index b08b6f1..0000000 --- a/test/input/func_raw_escapes.py +++ /dev/null @@ -1,19 +0,0 @@ -# pylint:disable=W0105, W0511, C0121 -"""Test for backslash escapes in byte vs unicode strings""" - -# Would be valid in Unicode, but probably not what you want otherwise -BAD_UNICODE = b'\u0042' -BAD_LONG_UNICODE = b'\U00000042' -BAD_NAMED_UNICODE = b'\N{GREEK SMALL LETTER ALPHA}' - -GOOD_UNICODE = u'\u0042' -GOOD_LONG_UNICODE = u'\U00000042' -GOOD_NAMED_UNICODE = u'\N{GREEK SMALL LETTER ALPHA}' - - -# Valid raw strings -RAW_BACKSLASHES = r'raw' -RAW_UNICODE = ur"\u0062\n" - -# In a comment you can have whatever you want: \ \\ \n \m -# even things that look like bad strings: "C:\Program Files" diff --git a/test/input/func_unicode_literal.py b/test/input/func_unicode_literal.py deleted file mode 100644 index fa47902..0000000 --- a/test/input/func_unicode_literal.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Unicode literals in Python 2.*""" -from __future__ import unicode_literals - -__revision__ = 0 - -BAD_STRING = b'\u1234' -GOOD_STRING = '\u1234' diff --git a/test/input/func_w0404.py b/test/input/func_w0404.py index 0e2f727..f475a4d 100644 --- a/test/input/func_w0404.py +++ b/test/input/func_w0404.py @@ -7,8 +7,8 @@ import sys import xml.etree.ElementTree from xml.etree import ElementTree -from multiprocessing import pool -import multiprocessing.pool +from email import encoders +import email.encoders import sys @@ -24,4 +24,4 @@ def reimport(): del sys -del sys, ElementTree, xml.etree.ElementTree, pool, multiprocessing.pool +del sys, ElementTree, xml.etree.ElementTree, encoders, email.encoders diff --git a/test/messages/func_bad_str_strip_call.txt b/test/messages/func_bad_str_strip_call.txt deleted file mode 100644 index 44b8780..0000000 --- a/test/messages/func_bad_str_strip_call.txt +++ /dev/null @@ -1,3 +0,0 @@ -E: 7: Suspicious argument in unicode.strip call -E: 8: Suspicious argument in unicode.lstrip call -E: 9: Suspicious argument in str.rstrip call diff --git a/test/messages/func_bad_str_strip_call_py30.txt b/test/messages/func_bad_str_strip_call_py30.txt deleted file mode 100644 index 8ea0372..0000000 --- a/test/messages/func_bad_str_strip_call_py30.txt +++ /dev/null @@ -1,3 +0,0 @@ -E: 7: Suspicious argument in str.strip call -E: 8: Suspicious argument in str.lstrip call -E: 9: Suspicious argument in bytes.rstrip call diff --git a/test/messages/func_name_checking.txt b/test/messages/func_name_checking.txt deleted file mode 100644 index e8d0a73..0000000 --- a/test/messages/func_name_checking.txt +++ /dev/null @@ -1,18 +0,0 @@ -C: 10:Run.B: Invalid class name "B" -C: 14:Run: Invalid variable name "bBb" -C: 28:HOHOHOHO: Invalid function name "HOHOHOHO" -C: 30:HOHOHOHO: Invalid variable name "HIHIHI" -C: 33:xyz: Invalid class name "xyz" -C: 36:xyz: Invalid class attribute name "zz" -C: 41:xyz.Youplapoum: Invalid method name "Youplapoum" -C: 56: Invalid constant name "benpasceluila" -C: 62:Correct.__init__: Invalid attribute name "_Ca_va_Pas" -C: 64:Correct.BadMethodName: Invalid method name "BadMethodName" -C: 77: Invalid class name "BAD_NAME_FOR_CLASS" -C: 78: Invalid class name "NEXT_BAD_NAME_FOR_CLASS" -C: 84: Invalid class name "NOT_CORRECT" -C: 90:test_globals: Invalid constant name "AlsoCorrect" -C:118:FooClass.PROPERTY_NAME: Invalid attribute name "PROPERTY_NAME" -C:123:FooClass.ABSTRACT_PROPERTY_NAME: Invalid attribute name "ABSTRACT_PROPERTY_NAME" -C:128:FooClass.PROPERTY_NAME_SETTER: Invalid attribute name "PROPERTY_NAME_SETTER" -C:133:func_bad_argname: Invalid argument name "NOT_GOOD" diff --git a/test/messages/func_namedtuple.txt b/test/messages/func_namedtuple.txt deleted file mode 100644 index d1ef2b1..0000000 --- a/test/messages/func_namedtuple.txt +++ /dev/null @@ -1 +0,0 @@ -E: 10: Class 'Thing' has no 'x' member diff --git a/test/messages/func_w0404.txt b/test/messages/func_w0404.txt index 73d770f..cd7f3e2 100644 --- a/test/messages/func_w0404.txt +++ b/test/messages/func_w0404.txt @@ -1,5 +1,5 @@ W: 8: Reimport 'ElementTree' (imported line 7) -W: 11: Reimport 'multiprocessing.pool' (imported line 10) +W: 11: Reimport 'email.encoders' (imported line 10) W: 13: Reimport 'sys' (imported line 5) W: 23:reimport: Redefining name 'sys' from outer scope (line 5) W: 23:reimport: Reimport 'sys' (imported line 5) diff --git a/test/test_func.py b/test/test_func.py index d3fb9be..6a64ec2 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -44,7 +44,7 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" - PORTED = set(['I0001', 'I0010', 'W0712', 'E1001']) + PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310']) @testlib.tag('coverage') def test_exhaustivity(self): diff --git a/test/test_functional.py b/test/test_functional.py index ae12360..5c5cafa 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -1,4 +1,5 @@ """Functional full-module tests for PyLint.""" +from __future__ import with_statement import ConfigParser import cStringIO import operator @@ -88,15 +89,15 @@ class TestFile(object): @property def expected_output(self): - return self._file_type('.txt') + return self._file_type('.txt', check_exists=False) @property def source(self): return self._file_type('.py') - def _file_type(self, ext): + def _file_type(self, ext, check_exists=True): name = os.path.join(self._directory, self.base + ext) - if os.path.exists(name): + if not check_exists or os.path.exists(name): return name else: raise NoFileError @@ -171,12 +172,12 @@ class LintModuleTest(testlib.TestCase): self._linter = lint.PyLinter() self._linter.set_reporter(test_reporter) self._linter.config.persistent = 0 + checkers.initialize(self._linter) self._linter.disable('I') try: self._linter.load_file_configuration(test_file.option_file) except NoFileError: pass - checkers.initialize(self._linter) self._test_file = test_file def shortDescription(self): diff --git a/test/unittest_checker_base.py b/test/unittest_checker_base.py index 972c783..1ac14b9 100644 --- a/test/unittest_checker_base.py +++ b/test/unittest_checker_base.py @@ -1,6 +1,8 @@ """Unittest for the base checker.""" +from __future__ import with_statement import re +import sys from astroid import test_utils from pylint.checkers import base @@ -80,6 +82,8 @@ class NameCheckerTest(CheckerTestCase): @set_config(attr_rgx=re.compile('[A-Z]+')) def test_property_names(self): + if sys.version_info < (2, 6): + self.skip('abc module does not exist on 2.5') # 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. diff --git a/test/unittest_checker_classes.py b/test/unittest_checker_classes.py index 388a24a..b185322 100644 --- a/test/unittest_checker_classes.py +++ b/test/unittest_checker_classes.py @@ -1,4 +1,5 @@ """Unit tests for the variables checker.""" +from __future__ import with_statement from astroid import test_utils from pylint.checkers import classes diff --git a/test/unittest_checker_format.py b/test/unittest_checker_format.py index b9fba5c..82b079a 100644 --- a/test/unittest_checker_format.py +++ b/test/unittest_checker_format.py @@ -15,6 +15,7 @@ Check format checker helper functions """ +from __future__ import with_statement import sys import re diff --git a/test/unittest_checker_logging.py b/test/unittest_checker_logging.py index cbacada..c36aec9 100644 --- a/test/unittest_checker_logging.py +++ b/test/unittest_checker_logging.py @@ -2,6 +2,7 @@ """ Unittest for the logging checker. """ +from __future__ import with_statement from logilab.common.testlib import unittest_main from astroid import test_utils diff --git a/test/unittest_checker_misc.py b/test/unittest_checker_misc.py index 670d53c..a494b1a 100644 --- a/test/unittest_checker_misc.py +++ b/test/unittest_checker_misc.py @@ -15,6 +15,8 @@ """ Tests for the misc checker. """ +from __future__ import with_statement + import sys import tempfile import os diff --git a/test/unittest_checker_typecheck.py b/test/unittest_checker_typecheck.py index 0a8687b..93c73ea 100644 --- a/test/unittest_checker_typecheck.py +++ b/test/unittest_checker_typecheck.py @@ -1,4 +1,5 @@ """Unittest for the type checker.""" +from __future__ import with_statement from astroid import test_utils from pylint.checkers import typecheck diff --git a/test/unittest_checker_variables.py b/test/unittest_checker_variables.py index 20a0d9e..a3776d7 100644 --- a/test/unittest_checker_variables.py +++ b/test/unittest_checker_variables.py @@ -1,4 +1,5 @@ """Unit tests for the variables checker.""" +from __future__ import with_statement import sys import os diff --git a/test/unittest_checkers_utils.py b/test/unittest_checkers_utils.py index 72108d7..40186f5 100644 --- a/test/unittest_checkers_utils.py +++ b/test/unittest_checkers_utils.py @@ -15,6 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """test the pylint.checkers.utils module """ +from __future__ import with_statement __revision__ = '$Id: unittest_checkers_utils.py,v 1.6 2005-11-02 09:22:07 syt Exp $' diff --git a/testutils.py b/testutils.py index daf7477..ef423c5 100644 --- a/testutils.py +++ b/testutils.py @@ -122,10 +122,30 @@ class TestReporter(BaseReporter): """ignore layouts""" -class Message(collections.namedtuple('Message', - ['msg_id', 'line', 'node', 'args'])): - def __new__(cls, msg_id, line=None, node=None, args=None): - return tuple.__new__(cls, (msg_id, line, node, args)) +if sys.version_info < (2, 6): + class Message(tuple): + def __new__(cls, msg_id, line=None, node=None, args=None): + return tuple.__new__(cls, (msg_id, line, node, args)) + + @property + def msg_id(self): + return self[0] + @property + def line(self): + return self[1] + @property + def node(self): + return self[2] + @property + def args(self): + return self[3] + + +else: + class Message(collections.namedtuple('Message', + ['msg_id', 'line', 'node', 'args'])): + def __new__(cls, msg_id, line=None, node=None, args=None): + return tuple.__new__(cls, (msg_id, line, node, args)) class UnittestLinter(object): -- cgit v1.2.1 From 640161196f431821182e4cfac5dd746a7ae464e4 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 25 Jul 2014 12:07:41 +0200 Subject: Extend the test case for metaclass invocation detection. --- test/input/func_ctor_arguments.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/input/func_ctor_arguments.py b/test/input/func_ctor_arguments.py index 7bfc3a2..f9a3430 100644 --- a/test/input/func_ctor_arguments.py +++ b/test/input/func_ctor_arguments.py @@ -64,8 +64,8 @@ ClassNew(one=2) class Metaclass(type): - pass - + def __new__(mcs, name, bases, namespace): + return type.__new__(mcs, name, bases, namespace) def with_metaclass(meta, base=object): """Create a new type that can be used as a metaclass.""" -- cgit v1.2.1 From 109090dfa67f1a7a35db5002346795bb717bf7d0 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Thu, 24 Jul 2014 18:21:12 +0200 Subject: extract a messages store from the MessagesHandlerMixIn class --- lint.py | 25 ++++--- reporters/__init__.py | 2 +- test/test_func.py | 6 +- test/test_regr.py | 4 +- test/unittest_lint.py | 156 +++++++++++++++++++++-------------------- utils.py | 188 ++++++++++++++++++++++++++------------------------ 6 files changed, 202 insertions(+), 179 deletions(-) diff --git a/lint.py b/lint.py index 0b7b44f..9814e1d 100644 --- a/lint.py +++ b/lint.py @@ -50,7 +50,7 @@ from astroid.modutils import load_module_from_name, get_module_part from pylint.utils import ( MSG_TYPES, OPTION_RGX, PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, - EmptyReport, WarningScope, + MessagesStore, EmptyReport, WarningScope, expand_modules, tokenize_module) from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker from pylint.checkers import (BaseTokenChecker, @@ -286,7 +286,8 @@ warning, statement which respectively contain the number of errors / warnings\ pylintrc=None): # some stuff has to be done before ancestors initialization... # - # checkers / reporter / astroid manager + # messages store / checkers / reporter / astroid manager + self.msgs_store = MessagesStore() self.reporter = None self._reporter_name = None self._reporters = {} @@ -423,11 +424,11 @@ warning, statement which respectively contain the number of errors / warnings\ self.register_report(r_id, r_title, r_cb, checker) self.register_options_provider(checker) if hasattr(checker, 'msgs'): - self.register_messages(checker) + self.msgs_store.register_messages(checker) checker.load_defaults() def disable_noerror_messages(self): - for msgcat, msgids in self._msgs_by_category.iteritems(): + for msgcat, msgids in self.msgs_store._msgs_by_category.iteritems(): if msgcat == 'E': for msgid in msgids: self.enable(msgid) @@ -529,7 +530,7 @@ warning, statement which respectively contain the number of errors / warnings\ if first <= lineno <= last: # Set state for all lines for this block, if the # warning is applied to nodes. - if self.check_message_id(msgid).scope == WarningScope.NODE: + if self.msgs_store.check_message_id(msgid).scope == WarningScope.NODE: if lineno > firstchildlineno: state = True first_, last_ = node.block_range(lineno) @@ -596,6 +597,12 @@ warning, statement which respectively contain the number of errors / warnings\ """main checking entry: check a list of files or modules from their name. """ + # initialize msgs_state now that all messages have been registered into + # the store + for msg in self.msgs_store.messages: + if not msg.may_be_emitted(): + self._msgs_state[msg.msgid] = False + if not isinstance(files_or_modules, (list, tuple)): files_or_modules = (files_or_modules,) walker = PyLintASTWalker(self) @@ -758,12 +765,12 @@ warning, statement which respectively contain the number of errors / warnings\ for line, enable in lines.iteritems(): if not enable and (warning, line) not in self._ignored_msgs: self.add_message('useless-suppression', line, None, - (self.get_msg_display_string(warning),)) + (self.msgs_store.get_msg_display_string(warning),)) # don't use iteritems here, _ignored_msgs may be modified by add_message for (warning, from_), lines in self._ignored_msgs.items(): for line in lines: self.add_message('suppressed-message', line, None, - (self.get_msg_display_string(warning), from_)) + (self.msgs_store.get_msg_display_string(warning), from_)) def report_evaluation(self, sect, stats, previous_stats): """make the global evaluation report""" @@ -1088,7 +1095,7 @@ are done by default'''}), def cb_help_message(self, option, optname, value, parser): """optik callback for printing some help about a particular message""" - self.linter.help_message(splitstrip(value)) + self.linter.msgs_store.help_message(splitstrip(value)) sys.exit(0) def cb_full_documentation(self, option, optname, value, parser): @@ -1098,7 +1105,7 @@ are done by default'''}), def cb_list_messages(self, option, optname, value, parser): # FIXME """optik callback for printing available messages""" - self.linter.list_messages() + self.linter.msgs_store.list_messages() sys.exit(0) def cb_init_hook(optname, value): diff --git a/reporters/__init__.py b/reporters/__init__.py index a767a05..12d193f 100644 --- a/reporters/__init__.py +++ b/reporters/__init__.py @@ -51,7 +51,7 @@ class Message(object): self.msg = msg self.C = msg_id[0] self.category = MSG_TYPES[msg_id[0]] - self.symbol = reporter.linter.check_message_id(msg_id).symbol + self.symbol = reporter.linter.msgs_store.check_message_id(msg_id).symbol def format(self, template): """Format the message according to the given template. diff --git a/test/test_func.py b/test/test_func.py index 6a64ec2..2d573c2 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2008 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 @@ -45,11 +45,11 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310']) - + @testlib.tag('coverage') def test_exhaustivity(self): # skip fatal messages - not_tested = set(msg.msgid for msg in linter.messages + not_tested = set(msg.msgid for msg in linter.msgs_store.messages if msg.msgid[0] != 'F' and msg.may_be_emitted()) for msgid in test_reporter.message_ids: try: diff --git a/test/test_regr.py b/test/test_regr.py index 0349481..8bc50c2 100644 --- a/test/test_regr.py +++ b/test/test_regr.py @@ -1,4 +1,4 @@ -# Copyright (c) 2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2005-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 @@ -60,7 +60,7 @@ class NonRegrTC(TestCase): got = linter.reporter.finalize().strip() checked = linter.stats['by_module'].keys() self.assertEqual(checked, ['package.__init__'], - '%s: %s' % (variation, checked)) + '%s: %s' % (variation, checked)) cwd = os.getcwd() os.chdir(join(REGR_DATA, 'package')) sys.path.insert(0, '') diff --git a/test/unittest_lint.py b/test/unittest_lint.py index a856d73..0e0ed9c 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -27,7 +27,8 @@ from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ - PyLintASTWalker, MessageDefinition, build_message_def, tokenize_module + MessagesStore, PyLintASTWalker, MessageDefinition, build_message_def, \ + tokenize_module from pylint.testutils import TestReporter from pylint.reporters import text from pylint import checkers @@ -60,51 +61,6 @@ class PyLinterTC(TestCase): checkers.initialize(self.linter) self.linter.set_reporter(TestReporter()) - def _compare_messages(self, desc, msg, checkerref=False): - # replace \r\n with \n, because - # logilab.common.textutils.normalize_text - # uses os.linesep, which will - # not properly compare with triple - # quoted multilines used in these tests - self.assertMultiLineEqual(desc, - msg.format_help(checkerref=checkerref) - .replace('\r\n', '\n')) - - def test_check_message_id(self): - self.assertIsInstance(self.linter.check_message_id('F0001'), - MessageDefinition) - self.assertRaises(UnknownMessage, - self.linter.check_message_id, 'YB12') - - def test_message_help(self): - msg = self.linter.check_message_id('F0001') - self._compare_messages( - ''':fatal (F0001): - Used when an error occurred preventing the analysis of a module (unable to - find it for instance). This message belongs to the master checker.''', - msg, checkerref=True) - self._compare_messages( - ''':fatal (F0001): - Used when an error occurred preventing the analysis of a module (unable to - find it for instance).''', - msg, checkerref=False) - - def test_message_help_minmax(self): - # build the message manually to be python version independant - 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 %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 %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) - def test_enable_message(self): linter = self.linter linter.open() @@ -246,16 +202,6 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('W0102', 1)) self.assertTrue(linter.is_message_enabled('dangerous-default-value', 1)) - def test_list_messages(self): - sys.stdout = StringIO() - try: - self.linter.list_messages() - output = sys.stdout.getvalue() - finally: - sys.stdout = sys.__stdout__ - # cursory examination of the output: we're mostly testing it completes - self.assertIn(':empty-docstring (C0112): *Empty %s docstring*', output) - def test_lint_ext_module_with_file_output(self): self.linter.set_reporter(text.TextReporter()) if sys.version_info < (3, 0): @@ -363,23 +309,6 @@ class PyLinterTC(TestCase): ['C: 1: Line too long (1/2)', 'C: 2: Line too long (3/4)'], self.linter.reporter.messages) - def test_add_renamed_message(self): - self.linter.add_renamed_message('C9999', 'old-bad-name', 'invalid-name') - self.assertEqual('invalid-name', - self.linter.check_message_id('C9999').symbol) - self.assertEqual('invalid-name', - self.linter.check_message_id('old-bad-name').symbol) - - def test_renamed_message_register(self): - class Checker(object): - msgs = {'W1234': ('message', 'msg-symbol', 'msg-description', - {'old_names': [('W0001', 'old-symbol')]})} - self.linter.register_messages(Checker()) - self.assertEqual('msg-symbol', - self.linter.check_message_id('W0001').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-hook', 'raise RuntimeError']) @@ -541,5 +470,86 @@ class PreprocessOptionsTC(TestCase): {'bar' : (None, False)}) +class MessagesStoreTC(TestCase): + def setUp(self): + self.store = MessagesStore() + class Checker(object): + name = 'achecker' + msgs = { + 'W1234': ('message', 'msg-symbol', 'msg description.', + {'old_names': [('W0001', 'old-symbol')]}), + 'E1234': ('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)}), + } + self.store.register_messages(Checker()) + + def _compare_messages(self, desc, msg, checkerref=False): + # replace \r\n with \n, because + # logilab.common.textutils.normalize_text + # uses os.linesep, which will + # not properly compare with triple + # quoted multilines used in these tests + self.assertMultiLineEqual( + desc, + msg.format_help(checkerref=checkerref).replace('\r\n', '\n')) + + def test_check_message_id(self): + self.assertIsInstance(self.store.check_message_id('W1234'), + MessageDefinition) + self.assertRaises(UnknownMessage, + self.store.check_message_id, 'YB12') + + def test_message_help(self): + msg = self.store.check_message_id('W1234') + self._compare_messages( + ''':msg-symbol (W1234): *message* + msg description. This message belongs to the achecker checker.''', + msg, checkerref=True) + self._compare_messages( + ''':msg-symbol (W1234): *message* + msg description.''', + msg, checkerref=False) + + def test_message_help_minmax(self): + # build the message manually to be python version independant + msg = self.store.check_message_id('E1234') + self._compare_messages( + ''':duplicate-keyword-arg (E1234): *Duplicate keyword argument %r in %s call* + Used when a function call passes the same keyword argument multiple times. + This message belongs to the achecker checker. It can't be emitted when using + Python >= 2.6.''', + msg, checkerref=True) + self._compare_messages( + ''':duplicate-keyword-arg (E1234): *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) + + def test_list_messages(self): + sys.stdout = StringIO() + try: + self.store.list_messages() + output = sys.stdout.getvalue() + finally: + sys.stdout = sys.__stdout__ + # cursory examination of the output: we're mostly testing it completes + self.assertIn(':msg-symbol (W1234): *message*', output) + + def test_add_renamed_message(self): + self.store.add_renamed_message('W1234', 'old-bad-name', 'msg-symbol') + self.assertEqual('msg-symbol', + self.store.check_message_id('W1234').symbol) + self.assertEqual('msg-symbol', + self.store.check_message_id('old-bad-name').symbol) + + def test_renamed_message_register(self): + self.assertEqual('msg-symbol', + self.store.check_message_id('W0001').symbol) + self.assertEqual('msg-symbol', + self.store.check_message_id('old-symbol').symbol) + + if __name__ == '__main__': unittest_main() diff --git a/utils.py b/utils.py index 6fbb480..9f5ffbd 100644 --- a/utils.py +++ b/utils.py @@ -190,62 +190,13 @@ class MessagesHandlerMixIn(object): """ def __init__(self): - # Primary registry for all active messages (i.e. all messages - # that can be emitted by pylint for the underlying Python - # version). It contains the 1:1 mapping from symbolic names - # to message definition objects. - self._messages = {} - # Maps alternative names (numeric IDs, deprecated names) to - # message definitions. May contain several names for each definition - # object. - self._alternative_names = {} self._msgs_state = {} self._module_msgs_state = {} # None self._raw_module_msgs_state = {} - self._msgs_by_category = {} self.msg_status = 0 self._ignored_msgs = {} self._suppression_mapping = {} - def add_renamed_message(self, old_id, old_symbol, new_symbol): - """Register the old ID and symbol for a warning that was renamed. - - This allows users to keep using the old ID/symbol in suppressions. - """ - msg = self.check_message_id(new_symbol) - msg.old_names.append((old_id, old_symbol)) - self._alternative_names[old_id] = msg - self._alternative_names[old_symbol] = msg - - def register_messages(self, checker): - """register a dictionary of messages - - Keys are message ids, values are a 2-uple with the message type and the - message itself - - message ids should be a string of len 4, where the two first characters - are the checker id and the two last the message id in this checker - """ - chkid = None - for msgid, msg_tuple in checker.msgs.iteritems(): - msg = build_message_def(checker, msgid, msg_tuple) - assert msg.symbol not in self._messages, \ - 'Message symbol %r is already defined' % msg.symbol - # avoid duplicate / malformed ids - assert msg.msgid not in self._alternative_names, \ - 'Message id %r is already defined' % msgid - assert chkid is None or chkid == msg.msgid[1:3], \ - 'Inconsistent checker part in message id %r' % msgid - chkid = msg.msgid[1:3] - if not msg.may_be_emitted(): - self._msgs_state[msg.msgid] = False - self._messages[msg.symbol] = msg - self._alternative_names[msg.msgid] = msg - for old_id, old_symbol in msg.old_names: - self._alternative_names[old_id] = msg - 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, ignore_unknown=False): """don't output message of the given id""" assert scope in ('package', 'module') @@ -257,14 +208,15 @@ class MessagesHandlerMixIn(object): # msgid is a category? catid = category_id(msgid) if catid is not None: - for _msgid in self._msgs_by_category.get(catid): + for _msgid in self.msgs_store._msgs_by_category.get(catid): self.disable(_msgid, scope, line) return # msgid is a checker name? if msgid.lower() in self._checkers: + msgs_store = self.msgs_store for checker in self._checkers[msgid.lower()]: for _msgid in checker.msgs: - if _msgid in self._alternative_names: + if _msgid in msgs_store._alternative_names: self.disable(_msgid, scope, line) return # msgid is report id? @@ -274,7 +226,7 @@ class MessagesHandlerMixIn(object): try: # msgid is a symbolic or numeric msgid. - msg = self.check_message_id(msgid) + msg = self.msgs_store.check_message_id(msgid) except UnknownMessage: if ignore_unknown: return @@ -303,7 +255,7 @@ class MessagesHandlerMixIn(object): catid = category_id(msgid) # msgid is a category? if catid is not None: - for msgid in self._msgs_by_category.get(catid): + for msgid in self.msgs_store._msgs_by_category.get(catid): self.enable(msgid, scope, line) return # msgid is a checker name? @@ -319,7 +271,7 @@ class MessagesHandlerMixIn(object): try: # msgid is a symbolic or numeric msgid. - msg = self.check_message_id(msgid) + msg = self.msgs_store.check_message_id(msgid) except UnknownMessage: if ignore_unknown: return @@ -337,30 +289,6 @@ class MessagesHandlerMixIn(object): msgs[msg.msgid] = True # sync configuration object self.config.enable = [mid for mid, val in msgs.iteritems() if val] - - def check_message_id(self, msgid): - """returns the Message object for this message. - - msgid may be either a numeric or symbolic id. - - Raises UnknownMessage if the message id is not defined. - """ - if msgid[1:].isdigit(): - msgid = msgid.upper() - for source in (self._alternative_names, self._messages): - try: - return source[msgid] - except KeyError: - pass - raise UnknownMessage('No such message id %s' % msgid) - - def get_msg_display_string(self, msgid): - """Generates a user-consumable representation of a message. - - Can be just the message ID or the ID and the symbol. - """ - return repr(self.check_message_id(msgid).symbol) - def get_message_state_scope(self, msgid, line=None): """Returns the scope at which a message was enabled/disabled.""" try: @@ -376,7 +304,7 @@ class MessagesHandlerMixIn(object): msgid may be either a numeric or symbolic message id. """ try: - msgid = self.check_message_id(msg_descr).msgid + msgid = self.msgs_store.check_message_id(msg_descr).msgid except UnknownMessage: # The linter checks for messages that are not registered # due to version mismatch, just treat them as message IDs @@ -412,7 +340,7 @@ class MessagesHandlerMixIn(object): provide line if the line number is different), raw and token checkers must provide the line argument. """ - msg_info = self.check_message_id(msg_descr) + msg_info = self.msgs_store.check_message_id(msg_descr) msgid = msg_info.msgid # backward compatibility, message may not have a symbol symbol = msg_info.symbol or msgid @@ -460,17 +388,6 @@ class MessagesHandlerMixIn(object): # add the message self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offset or 0), msg) - def help_message(self, msgids): - """display help messages for the given message identifiers""" - for msgid in msgids: - try: - print self.check_message_id(msgid).format_help(checkerref=True) - print - except UnknownMessage, ex: - print ex - print - continue - def print_full_documentation(self): """output a full documentation in ReST format""" by_checker = {} @@ -528,11 +445,100 @@ class MessagesHandlerMixIn(object): print print + +class MessagesStore(object): + """The messages store knows information about every possible message but has + no particular state during analysis. + """ + + def __init__(self): + # Primary registry for all active messages (i.e. all messages + # that can be emitted by pylint for the underlying Python + # version). It contains the 1:1 mapping from symbolic names + # to message definition objects. + self._messages = {} + # Maps alternative names (numeric IDs, deprecated names) to + # message definitions. May contain several names for each definition + # object. + self._alternative_names = {} + self._msgs_by_category = {} + @property def messages(self): """The list of all active messages.""" return self._messages.itervalues() + def add_renamed_message(self, old_id, old_symbol, new_symbol): + """Register the old ID and symbol for a warning that was renamed. + + This allows users to keep using the old ID/symbol in suppressions. + """ + msg = self.check_message_id(new_symbol) + msg.old_names.append((old_id, old_symbol)) + self._alternative_names[old_id] = msg + self._alternative_names[old_symbol] = msg + + def register_messages(self, checker): + """register a dictionary of messages + + Keys are message ids, values are a 2-uple with the message type and the + message itself + + message ids should be a string of len 4, where the two first characters + are the checker id and the two last the message id in this checker + """ + chkid = None + for msgid, msg_tuple in checker.msgs.iteritems(): + msg = build_message_def(checker, msgid, msg_tuple) + assert msg.symbol not in self._messages, \ + 'Message symbol %r is already defined' % msg.symbol + # avoid duplicate / malformed ids + assert msg.msgid not in self._alternative_names, \ + 'Message id %r is already defined' % msgid + assert chkid is None or chkid == msg.msgid[1:3], \ + 'Inconsistent checker part in message id %r' % msgid + chkid = msg.msgid[1:3] + self._messages[msg.symbol] = msg + self._alternative_names[msg.msgid] = msg + for old_id, old_symbol in msg.old_names: + self._alternative_names[old_id] = msg + self._alternative_names[old_symbol] = msg + self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid) + + def check_message_id(self, msgid): + """returns the Message object for this message. + + msgid may be either a numeric or symbolic id. + + Raises UnknownMessage if the message id is not defined. + """ + if msgid[1:].isdigit(): + msgid = msgid.upper() + for source in (self._alternative_names, self._messages): + try: + return source[msgid] + except KeyError: + pass + raise UnknownMessage('No such message id %s' % msgid) + + def get_msg_display_string(self, msgid): + """Generates a user-consumable representation of a message. + + Can be just the message ID or the ID and the symbol. + """ + return repr(self.check_message_id(msgid).symbol) + + def help_message(self, msgids): + """display help messages for the given message identifiers""" + for msgid in msgids: + try: + print self.check_message_id(msgid).format_help(checkerref=True) + print + except UnknownMessage, ex: + print ex + print + continue + def list_messages(self): """output full messages list documentation in ReST format""" msgs = sorted(self._messages.itervalues(), key=lambda msg: msg.msgid) -- cgit v1.2.1 From 2925504f4c4ffc3ec980eb7b4467284fe8653006 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Thu, 24 Jul 2014 23:53:44 +0200 Subject: [refactor] move handling of file specific state to its own FileState class (mostly message states) --- checkers/imports.py | 4 +- lint.py | 105 ++++---------------------- test/messages/func_i0022.txt | 9 +++ test/unittest_lint.py | 54 +++++++------- test/unittest_reporting.py | 2 +- utils.py | 171 ++++++++++++++++++++++++++++++++----------- 6 files changed, 182 insertions(+), 163 deletions(-) diff --git a/checkers/imports.py b/checkers/imports.py index 7194134..9ae3935 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -372,7 +372,7 @@ given file (report RP0402 must not be disabled)'} cache them """ if self.__ext_dep_info is None: - package = self.linter.base_name + package = self.linter.current_name self.__ext_dep_info = result = {} for importee, importers in self.stats['dependencies'].iteritems(): if not importee.startswith(package): @@ -384,7 +384,7 @@ given file (report RP0402 must not be disabled)'} cache them """ if self.__int_dep_info is None: - package = self.linter.base_name + package = self.linter.current_name self.__int_dep_info = result = {} for importee, importers in self.stats['dependencies'].iteritems(): if importee.startswith(package): diff --git a/lint.py b/lint.py index 9814e1d..4184985 100644 --- a/lint.py +++ b/lint.py @@ -50,7 +50,7 @@ from astroid.modutils import load_module_from_name, get_module_part from pylint.utils import ( MSG_TYPES, OPTION_RGX, PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, - MessagesStore, EmptyReport, WarningScope, + MessagesStore, FileState, EmptyReport, WarningScope, expand_modules, tokenize_module) from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker from pylint.checkers import (BaseTokenChecker, @@ -131,7 +131,7 @@ MSGS = { 'deprecated-pragma', 'Some inline pylint options have been renamed or reworked, ' 'only the most recent form should be used. ' - 'NOTE:skip-all is only available with pylint >= 0.26', + 'NOTE:skip-all is only available with pylint >= 0.26', {'old_names': [('I0014', 'deprecated-disable-all')]}), 'E0001': ('%s', @@ -294,8 +294,7 @@ warning, statement which respectively contain the number of errors / warnings\ self._checkers = {} self._ignore_file = False # visit variables - self.base_name = None - self.base_file = None + self.file_state = FileState() self.current_name = None self.current_file = None self.stats = None @@ -498,59 +497,6 @@ warning, statement which respectively contain the number of errors / warnings\ else: self.add_message('unrecognized-inline-option', args=opt, line=start[0]) - def collect_block_lines(self, node, msg_state): - """walk ast to collect block level options line numbers""" - # recurse on children (depth first) - for child in node.get_children(): - self.collect_block_lines(child, msg_state) - first = node.fromlineno - last = node.tolineno - # first child line number used to distinguish between disable - # which are the first child of scoped node with those defined later. - # For instance in the code below: - # - # 1. def meth8(self): - # 2. """test late disabling""" - # 3. # pylint: disable=E1102 - # 4. print self.blip - # 5. # pylint: disable=E1101 - # 6. print self.bla - # - # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6 - # - # this is necessary to disable locally messages applying to class / - # function using their fromlineno - if isinstance(node, (nodes.Module, nodes.Class, nodes.Function)) and node.body: - firstchildlineno = node.body[0].fromlineno - else: - firstchildlineno = last - for msgid, lines in msg_state.iteritems(): - for lineno, state in lines.items(): - original_lineno = lineno - if first <= lineno <= last: - # Set state for all lines for this block, if the - # warning is applied to nodes. - if self.msgs_store.check_message_id(msgid).scope == WarningScope.NODE: - if lineno > firstchildlineno: - state = True - first_, last_ = node.block_range(lineno) - else: - first_ = lineno - last_ = last - for line in xrange(first_, last_+1): - # do not override existing entries - if not line in self._module_msgs_state.get(msgid, ()): - if line in lines: # state change in the same block - state = lines[line] - original_lineno = line - if not state: - self._suppression_mapping[(msgid, line)] = original_lineno - try: - self._module_msgs_state[msgid][line] = state - except KeyError: - self._module_msgs_state[msgid] = {line: state} - del lines[lineno] - # code checking methods ################################################### @@ -571,7 +517,7 @@ warning, statement which respectively contain the number of errors / warnings\ if msg[0] != 'F' and self.is_message_enabled(msg)) if (messages or any(self.report_is_enabled(r[0]) for r in checker.reports)): - neededcheckers.append(checker) + neededcheckers.append(checker) # Sort checkers by priority neededcheckers = sorted(neededcheckers, key=attrgetter('priority'), reverse=True) @@ -628,14 +574,18 @@ warning, statement which respectively contain the number of errors / warnings\ astroid = self.get_ast(filepath, modname) if astroid is None: continue - self.base_name = descr['basename'] - self.base_file = descr['basepath'] + # XXX to be correct we need to keep module_msgs_state for every + # analyzed module (the problem stands with localized messages which + # are only detected in the .close step) + self.file_state = FileState(descr['basename']) 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 # pylint: disable=maybe-no-member self.check_astroid_module(astroid, walker, rawcheckers, tokencheckers) - self._add_suppression_messages() + # warn about spurious inline messages handling + for msgid, line, args in self.file_state.iter_spurious_suppression_messages(self.msgs_store): + self.add_message(msgid, line, None, args) # notify global end self.set_current_module('') self.stats['statement'] = walker.nbstatements @@ -669,13 +619,6 @@ warning, statement which respectively contain the number of errors / warnings\ self.stats['by_module'][modname]['statement'] = 0 for msg_cat in MSG_TYPES.itervalues(): self.stats['by_module'][modname][msg_cat] = 0 - # XXX hack, to be correct we need to keep module_msgs_state - # for every analyzed module (the problem stands with localized - # messages which are only detected in the .close step) - if modname: - self._module_msgs_state = {} - self._raw_module_msgs_state = {} - self._ignored_msgs = {} def get_ast(self, filepath, modname): """return a ast(roid) representation for a module""" @@ -709,12 +652,8 @@ warning, statement which respectively contain the number of errors / warnings\ if self._ignore_file: return False # walk ast to collect line numbers - for msg, lines in self._module_msgs_state.iteritems(): - self._raw_module_msgs_state[msg] = lines.copy() - orig_state = self._module_msgs_state.copy() - self._module_msgs_state = {} - self._suppression_mapping = {} - self.collect_block_lines(astroid, orig_state) + self.file_state.collect_block_lines(self.msgs_store, astroid) + # run raw and tokens checkers for checker in rawcheckers: checker.process_module(astroid) for checker in tokencheckers: @@ -738,9 +677,9 @@ warning, statement which respectively contain the number of errors / warnings\ if persistent run, pickle results for later comparison """ - if self.base_name is not None: + if self.file_state.base_name is not None: # load previous results if any - previous_stats = config.load_results(self.base_name) + previous_stats = config.load_results(self.file_state.base_name) # XXX code below needs refactoring to be more reporter agnostic self.reporter.on_close(self.stats, previous_stats) if self.config.reports: @@ -754,24 +693,12 @@ warning, statement which respectively contain the number of errors / warnings\ self.reporter.display_results(sect) # save results if persistent run if self.config.persistent: - config.save_results(self.stats, self.base_name) + config.save_results(self.stats, self.file_state.base_name) else: self.reporter.on_close(self.stats, {}) # specific reports ######################################################## - def _add_suppression_messages(self): - for warning, lines in self._raw_module_msgs_state.iteritems(): - for line, enable in lines.iteritems(): - if not enable and (warning, line) not in self._ignored_msgs: - self.add_message('useless-suppression', line, None, - (self.msgs_store.get_msg_display_string(warning),)) - # don't use iteritems here, _ignored_msgs may be modified by add_message - for (warning, from_), lines in self._ignored_msgs.items(): - for line in lines: - self.add_message('suppressed-message', line, None, - (self.msgs_store.get_msg_display_string(warning), from_)) - def report_evaluation(self, sect, stats, previous_stats): """make the global evaluation report""" # check with at least check 1 statements (usually 0 when there is a diff --git a/test/messages/func_i0022.txt b/test/messages/func_i0022.txt index ddcb066..fb527b1 100644 --- a/test/messages/func_i0022.txt +++ b/test/messages/func_i0022.txt @@ -1,12 +1,21 @@ I: 5: Locally disabling invalid-name (C0103) I: 5: Suppressed 'invalid-name' (from line 5) +I: 6: Locally disabling invalid-name (C0103) I: 6: Pragma "disable-msg" is deprecated, use "disable" instead I: 6: Suppressed 'invalid-name' (from line 6) +I: 8: Locally disabling invalid-name (C0103) I: 9: Suppressed 'invalid-name' (from line 8) +I: 10: Locally enabling invalid-name (C0103) +I: 12: Locally disabling invalid-name (C0103) I: 12: Pragma "disable-msg" is deprecated, use "disable" instead I: 13: Suppressed 'invalid-name' (from line 12) +I: 14: Locally enabling invalid-name (C0103) I: 14: Pragma "enable-msg" is deprecated, use "enable" instead +I: 16: Locally disabling invalid-name (C0103) I: 16: Pragma "disable-msg" is deprecated, use "disable" instead I: 17: Suppressed 'invalid-name' (from line 16) +I: 18: Locally enabling invalid-name (C0103) I: 18: Pragma "enable-msg" is deprecated, use "enable" instead +I: 20: Locally disabling invalid-name (C0103) I: 21: Suppressed 'invalid-name' (from line 20) +I: 22: Locally enabling invalid-name (C0103) diff --git a/test/unittest_lint.py b/test/unittest_lint.py index 0e0ed9c..d9185f6 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -27,8 +27,8 @@ from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ - MessagesStore, PyLintASTWalker, MessageDefinition, build_message_def, \ - tokenize_module + MessagesStore, PyLintASTWalker, MessageDefinition, FileState, \ + build_message_def, tokenize_module from pylint.testutils import TestReporter from pylint.reporters import text from pylint import checkers @@ -61,10 +61,15 @@ class PyLinterTC(TestCase): checkers.initialize(self.linter) self.linter.set_reporter(TestReporter()) - def test_enable_message(self): + def init_linter(self): linter = self.linter linter.open() linter.set_current_module('toto') + linter.file_state = FileState('toto') + return linter + + def test_enable_message(self): + linter = self.init_linter() self.assertTrue(linter.is_message_enabled('W0101')) self.assertTrue(linter.is_message_enabled('W0102')) linter.disable('W0101', scope='package') @@ -80,9 +85,7 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('W0102', 1)) def test_enable_message_category(self): - linter = self.linter - linter.open() - linter.set_current_module('toto') + linter = self.init_linter() self.assertTrue(linter.is_message_enabled('W0101')) self.assertTrue(linter.is_message_enabled('C0121')) linter.disable('W', scope='package') @@ -100,31 +103,29 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('C0121', line=1)) def test_message_state_scope(self): - linter = self.linter - linter.open() + linter = self.init_linter() + fs = linter.file_state linter.disable('C0121') self.assertEqual(MSG_STATE_SCOPE_CONFIG, - linter.get_message_state_scope('C0121')) + fs._message_state_scope('C0121')) linter.disable('W0101', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_CONFIG, - linter.get_message_state_scope('C0121')) + fs._message_state_scope('C0121')) self.assertEqual(MSG_STATE_SCOPE_MODULE, - linter.get_message_state_scope('W0101', 3)) + fs._message_state_scope('W0101', 3)) linter.enable('W0102', scope='module', line=3) self.assertEqual(MSG_STATE_SCOPE_MODULE, - linter.get_message_state_scope('W0102', 3)) + fs._message_state_scope('W0102', 3)) def test_enable_message_block(self): - linter = self.linter + linter = self.init_linter() linter.open() filepath = join(INPUTDIR, 'func_block_disable_msg.py') linter.set_current_module('func_block_disable_msg') astroid = linter.get_ast(filepath, 'func_block_disable_msg') linter.process_tokens(tokenize_module(astroid)) - orig_state = linter._module_msgs_state.copy() - linter._module_msgs_state = {} - linter._suppression_mapping = {} - linter.collect_block_lines(astroid, orig_state) + fs = linter.file_state + fs.collect_block_lines(linter.msgs_store, astroid) # global (module level) self.assertTrue(linter.is_message_enabled('W0613')) self.assertTrue(linter.is_message_enabled('E1101')) @@ -161,25 +162,24 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('E1101', 75)) self.assertTrue(linter.is_message_enabled('E1101', 77)) - self.assertEqual(17, linter._suppression_mapping['W0613', 18]) - self.assertEqual(30, linter._suppression_mapping['E1101', 33]) - self.assertTrue(('E1101', 46) not in linter._suppression_mapping) - self.assertEqual(1, linter._suppression_mapping['C0302', 18]) - self.assertEqual(1, linter._suppression_mapping['C0302', 50]) + fs = linter.file_state + self.assertEqual(17, fs._suppression_mapping['W0613', 18]) + self.assertEqual(30, fs._suppression_mapping['E1101', 33]) + self.assertTrue(('E1101', 46) not in fs._suppression_mapping) + self.assertEqual(1, fs._suppression_mapping['C0302', 18]) + self.assertEqual(1, fs._suppression_mapping['C0302', 50]) # This is tricky. While the disable in line 106 is disabling # both 108 and 110, this is usually not what the user wanted. # Therefore, we report the closest previous disable comment. - self.assertEqual(106, linter._suppression_mapping['E1101', 108]) - self.assertEqual(109, linter._suppression_mapping['E1101', 110]) + self.assertEqual(106, fs._suppression_mapping['E1101', 108]) + self.assertEqual(109, fs._suppression_mapping['E1101', 110]) def test_enable_by_symbol(self): """messages can be controlled by symbolic names. The state is consistent across symbols and numbers. """ - linter = self.linter - linter.open() - linter.set_current_module('toto') + linter = self.init_linter() self.assertTrue(linter.is_message_enabled('W0101')) self.assertTrue(linter.is_message_enabled('unreachable')) self.assertTrue(linter.is_message_enabled('W0102')) diff --git a/test/unittest_reporting.py b/test/unittest_reporting.py index 3dd0d0a..e1cf161 100644 --- a/test/unittest_reporting.py +++ b/test/unittest_reporting.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 diff --git a/utils.py b/utils.py index 9f5ffbd..65e9ce0 100644 --- a/utils.py +++ b/utils.py @@ -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 @@ -191,11 +191,7 @@ class MessagesHandlerMixIn(object): def __init__(self): self._msgs_state = {} - self._module_msgs_state = {} # None - self._raw_module_msgs_state = {} self.msg_status = 0 - self._ignored_msgs = {} - self._suppression_mapping = {} def disable(self, msgid, scope='package', line=None, ignore_unknown=False): """don't output message of the given id""" @@ -233,14 +229,10 @@ class MessagesHandlerMixIn(object): raise if scope == 'module': - assert line > 0 - try: - self._module_msgs_state[msg.msgid][line] = False - except KeyError: - self._module_msgs_state[msg.msgid] = {line: False} - if msg.symbol != 'locally-disabled': - self.add_message('locally-disabled', line=line, - args=(msg.symbol, msg.msgid)) + self.file_state.set_msg_status(msg, line, False) + if msg.symbol != 'locally-disabled': + self.add_message('locally-disabled', line=line, + args=(msg.symbol, msg.msgid)) else: msgs = self._msgs_state @@ -278,24 +270,13 @@ class MessagesHandlerMixIn(object): raise if scope == 'module': - assert line > 0 - try: - self._module_msgs_state[msg.msgid][line] = True - except KeyError: - self._module_msgs_state[msg.msgid] = {line: True} - self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid)) + self.file_state.set_msg_status(msg, line, True) + self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid)) else: msgs = self._msgs_state msgs[msg.msgid] = True # sync configuration object self.config.enable = [mid for mid, val in msgs.iteritems() if val] - def get_message_state_scope(self, msgid, line=None): - """Returns the scope at which a message was enabled/disabled.""" - try: - if line in self._module_msgs_state[msgid]: - return MSG_STATE_SCOPE_MODULE - except (KeyError, TypeError): - return MSG_STATE_SCOPE_CONFIG def is_message_enabled(self, msg_descr, line=None): """return true if the message associated to the given message id is @@ -313,24 +294,10 @@ class MessagesHandlerMixIn(object): if line is None: return self._msgs_state.get(msgid, True) try: - return self._module_msgs_state[msgid][line] - except (KeyError, TypeError): + return self.file_state._module_msgs_state[msgid][line] + except KeyError: return self._msgs_state.get(msgid, True) - def handle_ignored_message(self, state_scope, msgid, line, node, args): - """Report an ignored message. - - state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, - depending on whether the message was disabled locally in the module, - or globally. The other arguments are the same as for add_message. - """ - if state_scope == MSG_STATE_SCOPE_MODULE: - try: - orig_line = self._suppression_mapping[(msgid, line)] - self._ignored_msgs.setdefault((msgid, orig_line), set()).add(line) - except KeyError: - pass - def add_message(self, msg_descr, line=None, node=None, args=None): """Adds a message given by ID or name. @@ -362,8 +329,7 @@ class MessagesHandlerMixIn(object): col_offset = None # should this message be displayed if not self.is_message_enabled(msgid, line): - self.handle_ignored_message( - self.get_message_state_scope(msgid, line), msgid, line, node, args) + self.file_state.handle_ignored_message(msgid, line, node, args) return # update stats msg_cat = MSG_TYPES[msgid[0]] @@ -446,6 +412,123 @@ class MessagesHandlerMixIn(object): print +class FileState(object): + """Hold internal state specific to the currently analyzed file""" + + def __init__(self, modname=None): + self.base_name = modname + self._module_msgs_state = {} + self._raw_module_msgs_state = {} + self._ignored_msgs = {} + self._suppression_mapping = {} + + def collect_block_lines(self, msgs_store, module_node): + """Walk the AST to collect block level options line numbers.""" + for msg, lines in self._module_msgs_state.iteritems(): + self._raw_module_msgs_state[msg] = lines.copy() + orig_state = self._module_msgs_state.copy() + self._module_msgs_state = {} + self._suppression_mapping = {} + self._collect_block_lines(msgs_store, module_node, orig_state) + + def _collect_block_lines(self, msgs_store, node, msg_state): + """Recursivly walk (depth first) AST to collect block level options line + numbers. + """ + for child in node.get_children(): + self._collect_block_lines(msgs_store, child, msg_state) + first = node.fromlineno + last = node.tolineno + # first child line number used to distinguish between disable + # which are the first child of scoped node with those defined later. + # For instance in the code below: + # + # 1. def meth8(self): + # 2. """test late disabling""" + # 3. # pylint: disable=E1102 + # 4. print self.blip + # 5. # pylint: disable=E1101 + # 6. print self.bla + # + # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6 + # + # this is necessary to disable locally messages applying to class / + # function using their fromlineno + if isinstance(node, (nodes.Module, nodes.Class, nodes.Function)) and node.body: + firstchildlineno = node.body[0].fromlineno + else: + firstchildlineno = last + for msgid, lines in msg_state.iteritems(): + for lineno, state in lines.items(): + original_lineno = lineno + if first <= lineno <= last: + # Set state for all lines for this block, if the + # warning is applied to nodes. + if msgs_store.check_message_id(msgid).scope == WarningScope.NODE: + if lineno > firstchildlineno: + state = True + first_, last_ = node.block_range(lineno) + else: + first_ = lineno + last_ = last + for line in xrange(first_, last_+1): + # do not override existing entries + if not line in self._module_msgs_state.get(msgid, ()): + if line in lines: # state change in the same block + state = lines[line] + original_lineno = line + if not state: + self._suppression_mapping[(msgid, line)] = original_lineno + try: + self._module_msgs_state[msgid][line] = state + except KeyError: + self._module_msgs_state[msgid] = {line: state} + del lines[lineno] + + def set_msg_status(self, msg, line, status): + """Set status (enabled/disable) for a given message at a given line""" + assert line > 0 + try: + self._module_msgs_state[msg.msgid][line] = status + except KeyError: + self._module_msgs_state[msg.msgid] = {line: status} + + def handle_ignored_message(self, msgid, line, node, args): + """Report an ignored message. + + state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG, + depending on whether the message was disabled locally in the module, + or globally. The other arguments are the same as for add_message. + """ + state_scope = self._message_state_scope(msgid, line) + if state_scope == MSG_STATE_SCOPE_MODULE: + try: + orig_line = self._suppression_mapping[(msgid, line)] + self._ignored_msgs.setdefault((msgid, orig_line), set()).add(line) + except KeyError: + pass + + def _message_state_scope(self, msgid, line=None): + """Returns the scope at which a message was enabled/disabled.""" + try: + if line in self._module_msgs_state[msgid]: + return MSG_STATE_SCOPE_MODULE + except KeyError: + return MSG_STATE_SCOPE_CONFIG + + def iter_spurious_suppression_messages(self, msgs_store): + for warning, lines in self._raw_module_msgs_state.iteritems(): + for line, enable in lines.iteritems(): + if not enable and (warning, line) not in self._ignored_msgs: + yield 'useless-suppression', line, \ + (msgs_store.get_msg_display_string(warning),) + # don't use iteritems here, _ignored_msgs may be modified by add_message + for (warning, from_), lines in self._ignored_msgs.items(): + for line in lines: + yield 'suppressed-message', line, \ + (msgs_store.get_msg_display_string(warning), from_) + + class MessagesStore(object): """The messages store knows information about every possible message but has no particular state during analysis. -- cgit v1.2.1 From ee26e69c938d62c3fc48cd6848bee35ccce70637 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 25 Jul 2014 13:06:52 +0200 Subject: delete some trailing whitespaces --- test/test_functional.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_functional.py b/test/test_functional.py index 5c5cafa..bd65ce3 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -22,7 +22,7 @@ UPDATE = False # Common sub-expressions. _MESSAGE = {'msg': r'[a-z][a-z\-]+'} -# Matches a #, +# Matches a #, # - followed by a comparison operator and a Python version (optional), # - followed by an line number with a +/- (optional), # - followed by a list of bracketed message symbols. @@ -40,7 +40,7 @@ def parse_python_version(str): class TestReporter(reporters.BaseReporter): def add_message(self, msg_id, location, msg): self.messages.append(reporters.Message(self, msg_id, location, msg)) - + def on_set_current_module(self, module, filepath): self.messages = [] @@ -189,7 +189,7 @@ class LintModuleTest(testlib.TestCase): def _get_expected(self): with open(self._test_file.source) as fobj: expected = get_expected_messages(fobj) - + lines = [] if self._produces_output() and expected: with open(self._test_file.expected_output, 'U') as fobj: @@ -201,7 +201,7 @@ class LintModuleTest(testlib.TestCase): return expected, ''.join(lines) def _get_received(self): - messages = self._linter.reporter.messages + messages = self._linter.reporter.messages messages.sort(key=lambda m: (m.line, m.C, m.msg)) text_result = cStringIO.StringIO() received = {} @@ -220,7 +220,7 @@ class LintModuleTest(testlib.TestCase): if expected_messages != received_messages: msg = ['Wrong results for file "%s":' % (self._test_file.base)] - missing, unexpected = multiset_difference(expected_messages, + missing, unexpected = multiset_difference(expected_messages, received_messages) if missing: msg.append('\nExpected in testdata:') @@ -250,7 +250,7 @@ def active_in_running_python_version(options): def suite(): - input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), + input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'functional') suite = testlib.TestSuite() for fname in os.listdir(input_dir): -- cgit v1.2.1 From 4449a950ea759331f0840435616be7591fa407a0 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 25 Jul 2014 13:07:34 +0200 Subject: Don't emit hidden-method message when the attribute has been monkey-patched, you're on your own when you do that Closes http://www.logilab.org/ticket/36653 --- ChangeLog | 3 +++ checkers/classes.py | 10 +++++++--- test/functional/method_hidden.py | 15 +++++++++++++++ test/functional/method_hidden.txt | 1 + test/input/func_e0205.py | 17 ----------------- test/messages/func_e0205.txt | 2 -- test/test_func.py | 2 +- 7 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 test/functional/method_hidden.py create mode 100644 test/functional/method_hidden.txt delete mode 100644 test/input/func_e0205.py delete mode 100644 test/messages/func_e0205.txt diff --git a/ChangeLog b/ChangeLog index 9802d17..fcd60f6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -58,6 +58,9 @@ ChangeLog for Pylint * Use the proper mode for pickle when opening and writing the stats file. Closes issue #148. + * Don't emit hidden-method message when the attribute has been + monkey-patched, you're on your own when you do that + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the diff --git a/checkers/classes.py b/checkers/classes.py index 570f7ac..6d9b2df 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -20,7 +20,7 @@ from __future__ import generators import sys import astroid -from astroid import YES, Instance, are_exclusive, AssAttr +from astroid import YES, Instance, are_exclusive, AssAttr, Class from astroid.bases import Generator from pylint.interfaces import IAstroidChecker @@ -341,8 +341,12 @@ a metaclass class method.'} # check if the method is hidden by an attribute try: overridden = klass.instance_attr(node.name)[0] # XXX - args = (overridden.root().name, overridden.fromlineno) - self.add_message('method-hidden', args=args, node=node) + overridden_frame = overridden.frame() + if overridden_frame.type == 'method': + overridden_frame = overridden_frame.parent.frame() + if isinstance(overridden_frame, Class) and klass._is_subtype_of(overridden_frame.qname()): + args = (overridden.root().name, overridden.fromlineno) + self.add_message('method-hidden', args=args, node=node) except astroid.NotFoundError: pass diff --git a/test/functional/method_hidden.py b/test/functional/method_hidden.py new file mode 100644 index 0000000..0510f55 --- /dev/null +++ b/test/functional/method_hidden.py @@ -0,0 +1,15 @@ +# pylint: disable=too-few-public-methods +"""check method hidding ancestor attribute +""" + +class Abcd(object): + """dummy""" + def __init__(self): + self.abcd = 1 + +class Cdef(Abcd): + """dummy""" + def abcd(self): # [method-hidden] + """test + """ + print self diff --git a/test/functional/method_hidden.txt b/test/functional/method_hidden.txt new file mode 100644 index 0000000..c15f3bc --- /dev/null +++ b/test/functional/method_hidden.txt @@ -0,0 +1 @@ +method-hidden:12:Cdef.abcd:An attribute defined in functional.method_hidden line 8 hides this method diff --git a/test/input/func_e0205.py b/test/input/func_e0205.py deleted file mode 100644 index 34208f9..0000000 --- a/test/input/func_e0205.py +++ /dev/null @@ -1,17 +0,0 @@ -# pylint: disable=R0903 -"""check method hidding ancestor attribute -""" - -__revision__ = '' - -class Abcd(object): - """dummy""" - def __init__(self): - self.abcd = 1 - -class Cdef(Abcd): - """dummy""" - def abcd(self): - """test - """ - print self diff --git a/test/messages/func_e0205.txt b/test/messages/func_e0205.txt deleted file mode 100644 index c7402ce..0000000 --- a/test/messages/func_e0205.txt +++ /dev/null @@ -1,2 +0,0 @@ -E: 14:Cdef.abcd: An attribute defined in input.func_e0205 line 10 hides this method - diff --git a/test/test_func.py b/test/test_func.py index 2d573c2..8a0d983 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -44,7 +44,7 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" - PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310']) + PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310', 'E0202']) @testlib.tag('coverage') def test_exhaustivity(self): -- cgit v1.2.1 From a4fd0cec48aae5cba7ad289b36dd391def321fc7 Mon Sep 17 00:00:00 2001 From: Michal Nowikowski Date: Fri, 25 Jul 2014 13:49:28 +0200 Subject: Added two checkers for mixed line endings and for unexpected line endings. --- checkers/format.py | 34 +++++++++++++++++++++++++++++++--- test/functional/line_endings.args | 2 ++ test/functional/line_endings.py | 4 ++++ test/functional/line_endings.txt | 0 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 test/functional/line_endings.args create mode 100644 test/functional/line_endings.py create mode 100644 test/functional/line_endings.txt diff --git a/checkers/format.py b/checkers/format.py index c07ff32..858dff7 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -104,7 +104,13 @@ MSGS = { 'bracket or block opener.'), {'old_names': [('C0323', 'no-space-after-operator'), ('C0324', 'no-space-after-comma'), - ('C0322', 'no-space-before-operator')]}) + ('C0322', 'no-space-before-operator')]}), + 'C0327': ('Mixed line endings LF and CRLF', + 'mixed-line-endings', + 'Used when there are mixed (LF and CRLF) newline signs in a file.'), + 'C0328': ('Unexpected line ending format. There is \'%s\' while it should be \'%s\'.', + 'unexpected-line-ending-format', + 'Used when there is different newline sign than expected.'), } @@ -147,7 +153,7 @@ def _column_distance(token1, token2): def _last_token_on_line_is(tokens, line_end, token): return ( line_end > 0 and tokens.token(line_end-1) == token or - line_end > 1 and tokens.token(line_end-2) == token + line_end > 1 and tokens.token(line_end-2) == token and tokens.type(line_end-1) == tokenize.COMMENT) @@ -447,6 +453,10 @@ class FormatChecker(BaseTokenChecker): {'type': 'int', 'metavar': '', 'default': 4, 'help': 'Number of spaces of indent required inside a hanging ' ' or continued line.'}), + ('expected-line-ending-format', + {'type': 'choice', 'metavar': '', 'default': '', + 'choices': ['', 'LF', 'CRLF'], + 'help': 'Expected format of line ending, e.g. empty (any line ending), LF or CRLF.'}), ) def __init__(self, linter=None): @@ -454,6 +464,7 @@ class FormatChecker(BaseTokenChecker): self._lines = None self._visited_lines = None self._bracket_stack = [None] + self._last_line_ending = None def _pop_token(self): self._bracket_stack.pop() @@ -731,7 +742,7 @@ class FormatChecker(BaseTokenChecker): self.new_line(TokenWrapper(tokens), idx-1, idx+1) else: self.new_line(TokenWrapper(tokens), idx-1, idx) - + if tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form @@ -741,6 +752,7 @@ class FormatChecker(BaseTokenChecker): check_equal = True self._process_retained_warnings(TokenWrapper(tokens), idx) self._current_line.next_logical_line() + self._check_line_ending(token, line_num) elif tok_type == tokenize.INDENT: check_equal = False self.check_indent_level(token, indents[-1]+1, line_num) @@ -782,6 +794,22 @@ class FormatChecker(BaseTokenChecker): if line_num > self.config.max_module_lines: self.add_message('too-many-lines', args=line_num, line=1) + def _check_line_ending(self, line_ending, line_num): + # check if line endings are mixed + if self._last_line_ending is not None: + if line_ending != self._last_line_ending: + self.add_message('mixed-line-endings', line=line_num) + + self._last_line_ending = line_ending + + # check if line ending is as expected + expected = self.config.expected_line_ending_format + if expected: + line_ending = 'LF' if line_ending == '\n' else 'CRLF' + if line_ending != expected: + self.add_message('unexpected-line-ending-format', args=(line_ending, expected), line=line_num) + + def _process_retained_warnings(self, tokens, current_pos): single_line_block_stmt = not _last_token_on_line_is(tokens, current_pos, ':') diff --git a/test/functional/line_endings.args b/test/functional/line_endings.args new file mode 100644 index 0000000..95532ea --- /dev/null +++ b/test/functional/line_endings.args @@ -0,0 +1,2 @@ +[Format] +expected-line-ending-format=LF diff --git a/test/functional/line_endings.py b/test/functional/line_endings.py new file mode 100644 index 0000000..c8c7141 --- /dev/null +++ b/test/functional/line_endings.py @@ -0,0 +1,4 @@ +"mixing line endings are not welcome" + +CONST = 1 + diff --git a/test/functional/line_endings.txt b/test/functional/line_endings.txt new file mode 100644 index 0000000..e69de29 -- cgit v1.2.1 From e94abbd0d3a214b628133b496d9a65171e8ad382 Mon Sep 17 00:00:00 2001 From: Michal Nowikowski Date: Fri, 25 Jul 2014 14:07:18 +0200 Subject: Added two checkers for mixed line endings and for unexpected line endings. --- checkers/format.py | 34 +++++++++++++++++++++++++++++++--- test/functional/line_endings.args | 2 ++ test/functional/line_endings.py | 4 ++++ test/functional/line_endings.txt | 0 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 test/functional/line_endings.args create mode 100644 test/functional/line_endings.py create mode 100644 test/functional/line_endings.txt diff --git a/checkers/format.py b/checkers/format.py index c07ff32..858dff7 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -104,7 +104,13 @@ MSGS = { 'bracket or block opener.'), {'old_names': [('C0323', 'no-space-after-operator'), ('C0324', 'no-space-after-comma'), - ('C0322', 'no-space-before-operator')]}) + ('C0322', 'no-space-before-operator')]}), + 'C0327': ('Mixed line endings LF and CRLF', + 'mixed-line-endings', + 'Used when there are mixed (LF and CRLF) newline signs in a file.'), + 'C0328': ('Unexpected line ending format. There is \'%s\' while it should be \'%s\'.', + 'unexpected-line-ending-format', + 'Used when there is different newline sign than expected.'), } @@ -147,7 +153,7 @@ def _column_distance(token1, token2): def _last_token_on_line_is(tokens, line_end, token): return ( line_end > 0 and tokens.token(line_end-1) == token or - line_end > 1 and tokens.token(line_end-2) == token + line_end > 1 and tokens.token(line_end-2) == token and tokens.type(line_end-1) == tokenize.COMMENT) @@ -447,6 +453,10 @@ class FormatChecker(BaseTokenChecker): {'type': 'int', 'metavar': '', 'default': 4, 'help': 'Number of spaces of indent required inside a hanging ' ' or continued line.'}), + ('expected-line-ending-format', + {'type': 'choice', 'metavar': '', 'default': '', + 'choices': ['', 'LF', 'CRLF'], + 'help': 'Expected format of line ending, e.g. empty (any line ending), LF or CRLF.'}), ) def __init__(self, linter=None): @@ -454,6 +464,7 @@ class FormatChecker(BaseTokenChecker): self._lines = None self._visited_lines = None self._bracket_stack = [None] + self._last_line_ending = None def _pop_token(self): self._bracket_stack.pop() @@ -731,7 +742,7 @@ class FormatChecker(BaseTokenChecker): self.new_line(TokenWrapper(tokens), idx-1, idx+1) else: self.new_line(TokenWrapper(tokens), idx-1, idx) - + if tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form @@ -741,6 +752,7 @@ class FormatChecker(BaseTokenChecker): check_equal = True self._process_retained_warnings(TokenWrapper(tokens), idx) self._current_line.next_logical_line() + self._check_line_ending(token, line_num) elif tok_type == tokenize.INDENT: check_equal = False self.check_indent_level(token, indents[-1]+1, line_num) @@ -782,6 +794,22 @@ class FormatChecker(BaseTokenChecker): if line_num > self.config.max_module_lines: self.add_message('too-many-lines', args=line_num, line=1) + def _check_line_ending(self, line_ending, line_num): + # check if line endings are mixed + if self._last_line_ending is not None: + if line_ending != self._last_line_ending: + self.add_message('mixed-line-endings', line=line_num) + + self._last_line_ending = line_ending + + # check if line ending is as expected + expected = self.config.expected_line_ending_format + if expected: + line_ending = 'LF' if line_ending == '\n' else 'CRLF' + if line_ending != expected: + self.add_message('unexpected-line-ending-format', args=(line_ending, expected), line=line_num) + + def _process_retained_warnings(self, tokens, current_pos): single_line_block_stmt = not _last_token_on_line_is(tokens, current_pos, ':') diff --git a/test/functional/line_endings.args b/test/functional/line_endings.args new file mode 100644 index 0000000..95532ea --- /dev/null +++ b/test/functional/line_endings.args @@ -0,0 +1,2 @@ +[Format] +expected-line-ending-format=LF diff --git a/test/functional/line_endings.py b/test/functional/line_endings.py new file mode 100644 index 0000000..c8c7141 --- /dev/null +++ b/test/functional/line_endings.py @@ -0,0 +1,4 @@ +"mixing line endings are not welcome" + +CONST = 1 + diff --git a/test/functional/line_endings.txt b/test/functional/line_endings.txt new file mode 100644 index 0000000..e69de29 -- cgit v1.2.1 From d7f420156bc73fdc98484e83568df42f2c470dfb Mon Sep 17 00:00:00 2001 From: Michal Nowikowski Date: Fri, 25 Jul 2014 14:23:04 +0200 Subject: Fixed tests for line ending checks. --- test/functional/line_endings.py | 2 +- test/functional/line_endings.txt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/functional/line_endings.py b/test/functional/line_endings.py index c8c7141..d6bc3fc 100644 --- a/test/functional/line_endings.py +++ b/test/functional/line_endings.py @@ -1,4 +1,4 @@ "mixing line endings are not welcome" - +# +1: [unexpected-line-ending-format, mixed-line-endings] CONST = 1 diff --git a/test/functional/line_endings.txt b/test/functional/line_endings.txt index e69de29..a18a872 100644 --- a/test/functional/line_endings.txt +++ b/test/functional/line_endings.txt @@ -0,0 +1,2 @@ +mixed-line-endings:3::Mixed line endings LF and CRLF +unexpected-line-ending-format:3::Unexpected line ending format. There is 'CRLF' while it should be 'LF'. -- cgit v1.2.1 From 93da77dd6fa68b99cf9566de259fadd843255b69 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 25 Jul 2014 15:06:21 +0200 Subject: use a set instead of a dict with dumb values --- reporters/text.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reporters/text.py b/reporters/text.py index bd99837..04245f7 100644 --- a/reporters/text.py +++ b/reporters/text.py @@ -38,7 +38,7 @@ class TextReporter(BaseReporter): def __init__(self, output=None): BaseReporter.__init__(self, output) - self._modules = {} + self._modules = set() self._template = None def on_set_current_module(self, module, filepath): @@ -54,7 +54,7 @@ class TextReporter(BaseReporter): if m.module not in self._modules: if m.module: self.writeln('************* Module %s' % m.module) - self._modules[m.module] = 1 + self._modules.add(m.module) else: self.writeln('************* ') self.write_message(m) @@ -128,7 +128,7 @@ class ColorizedTextReporter(TextReporter): modsep = colorize_ansi('************* %s' % msg.module, color, style) self.writeln(modsep) - self._modules[msg.module] = 1 + self._modules.add(msg.module) color, style = self._get_decoration(msg.C) for attr in ('msg', 'symbol', 'category', 'C'): setattr(msg, attr, colorize_ansi(getattr(msg, attr), color, style)) -- cgit v1.2.1 From abdb701148386536b3af783056045550bd6a97a3 Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 25 Jul 2014 15:27:19 +0200 Subject: Only emit attribute-defined-outside-init for definition within the same module as the offended class This avoids mangling the output in some cases --- ChangeLog | 3 +++ checkers/classes.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index fcd60f6..33a784f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -61,6 +61,9 @@ ChangeLog for Pylint * Don't emit hidden-method message when the attribute has been monkey-patched, you're on your own when you do that + * Only emit attribute-defined-outside-init for definition within the same + module as the offended class, avoiding to mangle the output in some cases + 2014-04-30 -- 1.2.1 * Restore the ability to specify the init-hook option via the diff --git a/checkers/classes.py b/checkers/classes.py index 6d9b2df..170bb19 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -272,9 +272,13 @@ a metaclass class method.'} if not self.linter.is_message_enabled('attribute-defined-outside-init'): return defining_methods = self.config.defining_attr_methods + current_module = cnode.root() for attr, nodes in cnode.instance_attrs.iteritems(): + # skip nodes which are not in the current module and it may screw up + # the output, while it's not worth it nodes = [n for n in nodes if not - isinstance(n.statement(), (astroid.Delete, astroid.AugAssign))] + isinstance(n.statement(), (astroid.Delete, astroid.AugAssign)) + and n.root() is current_module] if not nodes: continue # error detected by typechecking # check if any method attr is defined in is a defining method -- cgit v1.2.1 From a288c6eceb47bca013b9c9ebf0c37e3260303654 Mon Sep 17 00:00:00 2001 From: Torsten Marek Date: Fri, 25 Jul 2014 15:32:17 +0200 Subject: Porting tests over to the new framework. --- test/functional/bad_continuation.py | 189 +++++++++++++++++++++++++++ test/functional/bad_continuation.txt | 63 +++++++++ test/functional/exception_is_binary_op.py | 12 ++ test/functional/exception_is_binary_op.txt | 4 + test/functional/redefined_builtin.py | 9 ++ test/functional/redefined_builtin.txt | 2 + test/functional/useless_else_on_loop.py | 55 ++++++++ test/functional/useless_else_on_loop.txt | 5 + test/input/func_bad_continuation.py | 191 ---------------------------- test/input/func_useless_else_on_loop.py | 55 -------- test/input/func_w0622.py | 11 -- test/input/func_w0711.py | 15 --- test/messages/func_bad_continuation.txt | 64 ---------- test/messages/func_useless_else_on_loop.txt | 5 - test/messages/func_w0711.txt | 4 - test/test_func.py | 3 +- 16 files changed, 341 insertions(+), 346 deletions(-) create mode 100644 test/functional/bad_continuation.py create mode 100644 test/functional/bad_continuation.txt create mode 100644 test/functional/exception_is_binary_op.py create mode 100644 test/functional/exception_is_binary_op.txt create mode 100644 test/functional/redefined_builtin.py create mode 100644 test/functional/redefined_builtin.txt create mode 100644 test/functional/useless_else_on_loop.py create mode 100644 test/functional/useless_else_on_loop.txt delete mode 100644 test/input/func_bad_continuation.py delete mode 100644 test/input/func_useless_else_on_loop.py delete mode 100644 test/input/func_w0622.py delete mode 100644 test/input/func_w0711.py delete mode 100644 test/messages/func_bad_continuation.txt delete mode 100644 test/messages/func_useless_else_on_loop.txt delete mode 100644 test/messages/func_w0711.txt diff --git a/test/functional/bad_continuation.py b/test/functional/bad_continuation.py new file mode 100644 index 0000000..3d62521 --- /dev/null +++ b/test/functional/bad_continuation.py @@ -0,0 +1,189 @@ +"""Regression test case for bad-continuation.""" + +# Various alignment for brackets +LIST0 = [ + 1, 2, 3 +] +LIST1 = [ + 1, 2, 3 + ] +LIST2 = [ + 1, 2, 3 + ] # [bad-continuation] + +# Alignment inside literals +W0 = [1, 2, 3, + 4, 5, 6, + 7, # [bad-continuation] + 8, 9, 10, + 11, 12, 13, + # and a comment + 14, 15, 16] + +W1 = { + 'a': 1, + 'b': 2, # [bad-continuation] + 'c': 3, + } + +W2 = { + 'a': 1, + 'b': 2, # [bad-continuation] + 'c': 3, + } + +W2 = ['some', 'contents' # with a continued comment that may be aligned + # under the previous comment (optionally) + 'and', + 'more', # but this + # [bad-continuation] is not accepted + 'contents', # [bad-continuation] nor this. + ] + +# Values in dictionaries should be indented 4 spaces further if they are on a +# different line than their key +W4 = { + 'key1': + 'value1', # Grandfather in the old style + 'key2': + 'value2', # [bad-continuation] + 'key3': + 'value3', # Comma here + } + +# And should follow the same rules as continuations within parens +W5 = { + 'key1': 'long value' + 'long continuation', + 'key2': 'breaking' + 'wrong', # [bad-continuation] + 'key3': 2*( + 2+2), + 'key4': ('parenthesis', + 'continuation') # No comma here + } + +# Allow values to line up with their keys when the key is next to the brace +W6 = {'key1': + 'value1', + 'key2': + 'value2', + } + +# Or allow them to be indented +W7 = {'key1': + 'value1', + 'key2': + 'value2' + } + +# Bug that caused a warning on the previous two cases permitted these odd +# incorrect indentations +W8 = {'key1': +'value1', # [bad-continuation] + } + +W9 = {'key1': + 'value1', # [bad-continuation] + } + +# Alignment of arguments in function definitions +def continue1(some_arg, + some_other_arg): + """A function with well-aligned arguments.""" + print some_arg, some_other_arg + + +def continue2( + some_arg, + some_other_arg): + """A function with well-aligned arguments.""" + print some_arg, some_other_arg + +def continue3( + some_arg, # [bad-continuation] + some_other_arg): # [bad-continuation] + """A function with misaligned arguments""" + print some_arg, some_other_arg + +def continue4( # pylint:disable=missing-docstring + arg1, + arg2): print arg1, arg2 + + +def callee(*args): + """noop""" + print args + + +callee( + "a", + "b" + ) + +callee("a", + "b") # [bad-continuation] + +callee(5, {'a': 'b', + 'c': 'd'}) + +if ( + 1 + ): pass + +if ( + 1 +): pass +if ( + 1 + ): pass # [bad-continuation] + +if (1 and + 2): # [bad-continuation] + pass + +while (1 and + 2): + pass + +while (1 and + 2 and # [bad-continuation] + 3): + pass + +if ( + 2): pass # [bad-continuation] + +if (1 or + 2 or + 3): pass + +if (1 or + 2 or # [bad-continuation] + 3): print 1, 2 + +if (1 and + 2): pass # [bad-continuation] + +if ( + 2): pass + +if ( + 2): # [bad-continuation] + pass + +L1 = (lambda a, + b: a + b) + +if not (1 and + 2): + print 3 + +if not (1 and + 2): # [bad-continuation] + print 3 + +continue2("foo", + some_other_arg="this " + "is " + "fine") diff --git a/test/functional/bad_continuation.txt b/test/functional/bad_continuation.txt new file mode 100644 index 0000000..732858e --- /dev/null +++ b/test/functional/bad_continuation.txt @@ -0,0 +1,63 @@ +bad-continuation:12::Wrong hanging indentation. + ] # [bad-continuation] +| ^| +bad-continuation:17::Wrong continued indentation. + 7, # [bad-continuation] + | ^ +bad-continuation:25::Wrong hanging indentation. + 'b': 2, # [bad-continuation] + ^| +bad-continuation:31::Wrong hanging indentation. + 'b': 2, # [bad-continuation] + ^| +bad-continuation:39::Wrong continued indentation. + # [bad-continuation] is not accepted + | | ^ +bad-continuation:40::Wrong continued indentation. + 'contents', # [bad-continuation] nor this. + | ^ +bad-continuation:49::Wrong hanging indentation in dict value. + 'value2', # [bad-continuation] + | ^ | +bad-continuation:59::Wrong continued indentation. + 'wrong', # [bad-continuation] + ^ | +bad-continuation:83::Wrong hanging indentation in dict value. +'value1', # [bad-continuation] +^ | | +bad-continuation:87::Wrong hanging indentation in dict value. + 'value1', # [bad-continuation] + ^ | | +bad-continuation:104::Wrong hanging indentation before block. + some_arg, # [bad-continuation] + ^ | +bad-continuation:105::Wrong hanging indentation before block. + some_other_arg): # [bad-continuation] + ^ | +bad-continuation:125::Wrong continued indentation. + "b") # [bad-continuation] + ^ | +bad-continuation:139::Wrong hanging indentation before block. + ): pass # [bad-continuation] +| ^| +bad-continuation:142::Wrong continued indentation before block. + 2): # [bad-continuation] + ^ | +bad-continuation:150::Wrong continued indentation. + 2 and # [bad-continuation] + | ^ +bad-continuation:155::Wrong hanging indentation before block. + 2): pass # [bad-continuation] + ^ | | +bad-continuation:162::Wrong continued indentation before block. + 2 or # [bad-continuation] + |^ | +bad-continuation:166::Wrong continued indentation before block. + 2): pass # [bad-continuation] + ^ | | +bad-continuation:172::Wrong hanging indentation before block. + 2): # [bad-continuation] + ^ | | +bad-continuation:183::Wrong continued indentation. + 2): # [bad-continuation] + ^ | diff --git a/test/functional/exception_is_binary_op.py b/test/functional/exception_is_binary_op.py new file mode 100644 index 0000000..443a478 --- /dev/null +++ b/test/functional/exception_is_binary_op.py @@ -0,0 +1,12 @@ +"""Warn about binary operations used as exceptions.""" + +try: + pass +except Exception or StandardError: # [binary-op-exception] + print "caught1" +except Exception and StandardError: # [binary-op-exception] + print "caught2" +except Exception or StandardError: # [binary-op-exception] + print "caught3" +except (Exception or StandardError), exc: # [binary-op-exception] + print "caught4" diff --git a/test/functional/exception_is_binary_op.txt b/test/functional/exception_is_binary_op.txt new file mode 100644 index 0000000..039bc3f --- /dev/null +++ b/test/functional/exception_is_binary_op.txt @@ -0,0 +1,4 @@ +binary-op-exception:5::Exception to catch is the result of a binary "or" operation +binary-op-exception:7::Exception to catch is the result of a binary "and" operation +binary-op-exception:9::Exception to catch is the result of a binary "or" operation +binary-op-exception:11::Exception to catch is the result of a binary "or" operation diff --git a/test/functional/redefined_builtin.py b/test/functional/redefined_builtin.py new file mode 100644 index 0000000..ace2ea6 --- /dev/null +++ b/test/functional/redefined_builtin.py @@ -0,0 +1,9 @@ +"""Tests for redefining builtins.""" + +def function(): + """Redefined local.""" + type = 1 # [redefined-builtin] + print type + +# pylint:disable=invalid-name +map = {} # [redefined-builtin] diff --git a/test/functional/redefined_builtin.txt b/test/functional/redefined_builtin.txt new file mode 100644 index 0000000..71518ce --- /dev/null +++ b/test/functional/redefined_builtin.txt @@ -0,0 +1,2 @@ +redefined-builtin:5:function:Redefining built-in 'type' +redefined-builtin:9::Redefining built-in 'map' diff --git a/test/functional/useless_else_on_loop.py b/test/functional/useless_else_on_loop.py new file mode 100644 index 0000000..4f27cf7 --- /dev/null +++ b/test/functional/useless_else_on_loop.py @@ -0,0 +1,55 @@ +"""Check for else branches on loops with break an return only.""" + +__revision__ = 0 + +def test_return_for(): + """else + return is not accetable.""" + for i in range(10): + if i % 2: + return i + else: # [useless-else-on-loop] + print 'math is broken' + +def test_return_while(): + """else + return is not accetable.""" + while True: + return 1 + else: # [useless-else-on-loop] + print 'math is broken' + + +while True: + def short_fun(): + """A function with a loop.""" + for _ in range(10): + break +else: # [useless-else-on-loop] + print 'or else!' + + +while True: + while False: + break +else: # [useless-else-on-loop] + print 'or else!' + +for j in range(10): + pass +else: # [useless-else-on-loop] + print 'fat chance' + for j in range(10): + break + +def test_return_for2(): + """no false positive for break in else + + https://bitbucket.org/logilab/pylint/issue/117/useless-else-on-loop-false-positives + """ + for i in range(10): + for i in range(i): + if i % 2: + break + else: + break + else: + print 'great math' diff --git a/test/functional/useless_else_on_loop.txt b/test/functional/useless_else_on_loop.txt new file mode 100644 index 0000000..93309b6 --- /dev/null +++ b/test/functional/useless_else_on_loop.txt @@ -0,0 +1,5 @@ +useless-else-on-loop:10:test_return_for:Else clause on loop without a break statement +useless-else-on-loop:17:test_return_while:Else clause on loop without a break statement +useless-else-on-loop:26::Else clause on loop without a break statement +useless-else-on-loop:33::Else clause on loop without a break statement +useless-else-on-loop:38::Else clause on loop without a break statement diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py deleted file mode 100644 index f303915..0000000 --- a/test/input/func_bad_continuation.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Regression test case for bad-continuation.""" - -__revision__ = 1 - -# Various alignment for brackets -LIST0 = [ - 1, 2, 3 -] -LIST1 = [ - 1, 2, 3 - ] -LIST2 = [ - 1, 2, 3 - ] # [bad-continuation] - -# Alignment inside literals -W0 = [1, 2, 3, - 4, 5, 6, - 7, # [bad-continuation] - 8, 9, 10, - 11, 12, 13, - # and a comment - 14, 15, 16] - -W1 = { - 'a': 1, - 'b': 2, # [bad-continuation] - 'c': 3, - } - -W2 = { - 'a': 1, - 'b': 2, # [bad-continuation] - 'c': 3, - } - -W2 = ['some', 'contents' # with a continued comment that may be aligned - # under the previous comment (optionally) - 'and', - 'more', # but this - # [bad-continuation] is not accepted - 'contents', # nor this. [bad-continuation] - ] - -# Values in dictionaries should be indented 4 spaces further if they are on a -# different line than their key -W4 = { - 'key1': - 'value1', # Grandfather in the old style - 'key2': - 'value2', # [bad-continuation] - 'key3': - 'value3', # Comma here - } - -# And should follow the same rules as continuations within parens -W5 = { - 'key1': 'long value' - 'long continuation', - 'key2': 'breaking' - 'wrong', # [bad-continuation] - 'key3': 2*( - 2+2), - 'key4': ('parenthesis', - 'continuation') # No comma here - } - -# Allow values to line up with their keys when the key is next to the brace -W6 = {'key1': - 'value1', - 'key2': - 'value2', - } - -# Or allow them to be indented -W7 = {'key1': - 'value1', - 'key2': - 'value2' - } - -# Bug that caused a warning on the previous two cases permitted these odd -# incorrect indentations -W8 = {'key1': -'value1', # [bad-continuation] - } - -W9 = {'key1': - 'value1', # [bad-continuation] - } - -# Alignment of arguments in function definitions -def continue1(some_arg, - some_other_arg): - """A function with well-aligned arguments.""" - print some_arg, some_other_arg - - -def continue2( - some_arg, - some_other_arg): - """A function with well-aligned arguments.""" - print some_arg, some_other_arg - -def continue3( - some_arg, # [bad-continuation] - some_other_arg): # [bad-continuation] - """A function with misaligned arguments""" - print some_arg, some_other_arg - -def continue4( - arg1, - arg2): print arg1, arg2 - - -def callee(*args): - """noop""" - print args - - -callee( - "a", - "b" - ) - -callee("a", - "b") # [bad-continuation] - -callee(5, {'a': 'b', - 'c': 'd'}) - -if ( - 1 - ): pass - -if ( - 1 -): pass -if ( - 1 - ): pass # [bad-continuation] - -if (1 and - 2): # [bad-continuation] - pass - -while (1 and - 2): - pass - -while (1 and - 2 and # [bad-continuation] - 3): - pass - -if ( - 2): pass # [bad-continuation] - -if (1 or - 2 or - 3): pass - -if (1 or - 2 or # [bad-continuation] - 3): print 1, 2 - -if (1 and - 2): pass # [bad-continuation] - -if ( - 2): pass - -if ( - 2): # [bad-continuation] - pass - -L1 = (lambda a, - b: a + b) - -if not (1 and - 2): - print 3 - -if not (1 and - 2): # [bad-continuation] - print 3 - -continue2("foo", - some_other_arg="this " - "is " - "fine") diff --git a/test/input/func_useless_else_on_loop.py b/test/input/func_useless_else_on_loop.py deleted file mode 100644 index 289366a..0000000 --- a/test/input/func_useless_else_on_loop.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Check for else branches on loops with break an return only.""" - -__revision__ = 0 - -def test_return_for(): - """else + return is not accetable.""" - for i in range(10): - if i % 2: - return i - else: - print 'math is broken' - -def test_return_while(): - """else + return is not accetable.""" - while True: - return 1 - else: - print 'math is broken' - - -while True: - def short_fun(): - """A function with a loop.""" - for _ in range(10): - break -else: - print 'or else!' - - -while True: - while False: - break -else: - print 'or else!' - -for j in range(10): - pass -else: - print 'fat chance' - for j in range(10): - break - -def test_return_for2(): - """no false positive for break in else - - https://bitbucket.org/logilab/pylint/issue/117/useless-else-on-loop-false-positives - """ - for i in range(10): - for i in range(i): - if i % 2: - break - else: - break - else: - print 'great math' diff --git a/test/input/func_w0622.py b/test/input/func_w0622.py deleted file mode 100644 index 5769841..0000000 --- a/test/input/func_w0622.py +++ /dev/null @@ -1,11 +0,0 @@ -# pylint: disable=C0103 -"""test built-in redefinition -""" -__revision__ = 0 - -def function(): - """yo""" - type = 1 - print type - -map = {} diff --git a/test/input/func_w0711.py b/test/input/func_w0711.py deleted file mode 100644 index 97c3697..0000000 --- a/test/input/func_w0711.py +++ /dev/null @@ -1,15 +0,0 @@ -"""find binary operations used as exceptions -""" - -__revision__ = 1 - -try: - __revision__ += 1 -except Exception or StandardError: - print "caught1" -except Exception and StandardError: - print "caught2" -except Exception or StandardError: - print "caught3" -except (Exception or StandardError), exc: - print "caught4" diff --git a/test/messages/func_bad_continuation.txt b/test/messages/func_bad_continuation.txt deleted file mode 100644 index 42087e5..0000000 --- a/test/messages/func_bad_continuation.txt +++ /dev/null @@ -1,64 +0,0 @@ -C: 14: Wrong hanging indentation. - ] # [bad-continuation] -| ^| -C: 19: Wrong continued indentation. - 7, # [bad-continuation] - | ^ -C: 27: Wrong hanging indentation. - 'b': 2, # [bad-continuation] - ^| -C: 33: Wrong hanging indentation. - 'b': 2, # [bad-continuation] - ^| -C: 41: Wrong continued indentation. - # [bad-continuation] is not accepted - | | ^ -C: 42: Wrong continued indentation. - 'contents', # nor this. [bad-continuation] - | ^ -C: 51: Wrong hanging indentation in dict value. - 'value2', # [bad-continuation] - | ^ | -C: 61: Wrong continued indentation. - 'wrong', # [bad-continuation] - ^ | -C: 85: Wrong hanging indentation in dict value. -'value1', # [bad-continuation] -^ | | -C: 89: Wrong hanging indentation in dict value. - 'value1', # [bad-continuation] - ^ | | -C:106: Wrong hanging indentation before block. - some_arg, # [bad-continuation] - ^ | -C:107: Wrong hanging indentation before block. - some_other_arg): # [bad-continuation] - ^ | -C:111:continue4: Missing function docstring -C:127: Wrong continued indentation. - "b") # [bad-continuation] - ^ | -C:141: Wrong hanging indentation before block. - ): pass # [bad-continuation] -| ^| -C:144: Wrong continued indentation before block. - 2): # [bad-continuation] - ^ | -C:152: Wrong continued indentation. - 2 and # [bad-continuation] - | ^ -C:157: Wrong hanging indentation before block. - 2): pass # [bad-continuation] - ^ | | -C:164: Wrong continued indentation before block. - 2 or # [bad-continuation] - |^ | -C:168: Wrong continued indentation before block. - 2): pass # [bad-continuation] - ^ | | -C:174: Wrong hanging indentation before block. - 2): # [bad-continuation] - ^ | | -C:185: Wrong continued indentation. - 2): # [bad-continuation] - ^ | diff --git a/test/messages/func_useless_else_on_loop.txt b/test/messages/func_useless_else_on_loop.txt deleted file mode 100644 index 6c2c60c..0000000 --- a/test/messages/func_useless_else_on_loop.txt +++ /dev/null @@ -1,5 +0,0 @@ -W: 10:test_return_for: Else clause on loop without a break statement -W: 17:test_return_while: Else clause on loop without a break statement -W: 26: Else clause on loop without a break statement -W: 33: Else clause on loop without a break statement -W: 38: Else clause on loop without a break statement diff --git a/test/messages/func_w0711.txt b/test/messages/func_w0711.txt deleted file mode 100644 index a158818..0000000 --- a/test/messages/func_w0711.txt +++ /dev/null @@ -1,4 +0,0 @@ -W: 8: Exception to catch is the result of a binary "or" operation -W: 10: Exception to catch is the result of a binary "and" operation -W: 12: Exception to catch is the result of a binary "or" operation -W: 14: Exception to catch is the result of a binary "or" operation diff --git a/test/test_func.py b/test/test_func.py index 8a0d983..c6394a1 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -44,7 +44,8 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" - PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310', 'E0202']) + PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310', 'E0202', + 'W0711']) @testlib.tag('coverage') def test_exhaustivity(self): -- cgit v1.2.1 From f99a41b44da0fc78682081393fdda3644ee7b799 Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 25 Jul 2014 17:13:50 +0200 Subject: Don't emit 'unnecessary-lambda' if the body of the lambda call contains call chaining. Closes issue #243. --- ChangeLog | 11 +++++--- checkers/base.py | 5 ++++ test/functional/unnecessary_lambda.py | 50 ++++++++++++++++++++++++++++++++++ test/functional/unnecessary_lambda.txt | 7 +++++ test/input/func_w0108.py | 40 --------------------------- test/messages/func_w0108.txt | 8 ------ 6 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 test/functional/unnecessary_lambda.py create mode 100644 test/functional/unnecessary_lambda.txt delete mode 100644 test/input/func_w0108.py delete mode 100644 test/messages/func_w0108.txt diff --git a/ChangeLog b/ChangeLog index 33a784f..4f91ebf 100644 --- a/ChangeLog +++ b/ChangeLog @@ -58,11 +58,14 @@ ChangeLog for Pylint * Use the proper mode for pickle when opening and writing the stats file. Closes issue #148. - * Don't emit hidden-method message when the attribute has been - monkey-patched, you're on your own when you do that + * Don't emit hidden-method message when the attribute has been + monkey-patched, you're on your own when you do that. - * Only emit attribute-defined-outside-init for definition within the same - module as the offended class, avoiding to mangle the output in some cases + * Only emit attribute-defined-outside-init for definition within the same + module as the offended class, avoiding to mangle the output in some cases. + + * Don't emit 'unnecessary-lambda' if the body of the lambda call contains + call chaining. Closes issue #243. 2014-04-30 -- 1.2.1 diff --git a/checkers/base.py b/checkers/base.py index 63b153b..1a41f59 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -630,6 +630,11 @@ functions, methods return if node.args.args[i].name != call.args[i].name: return + if (isinstance(node.body.func, astroid.Getattr) and + isinstance(node.body.func.expr, astroid.CallFunc)): + # Chained call, the intermediate call might + # return something else (but we don't check that, yet). + return self.add_message('unnecessary-lambda', line=node.fromlineno, node=node) @check_messages('dangerous-default-value') diff --git a/test/functional/unnecessary_lambda.py b/test/functional/unnecessary_lambda.py new file mode 100644 index 0000000..1c9b424 --- /dev/null +++ b/test/functional/unnecessary_lambda.py @@ -0,0 +1,50 @@ +# pylint: disable=star-args, undefined-variable +"""test suspicious lambda expressions +""" + +__revision__ = '' + +# Some simple examples of the most commonly encountered forms. +# +1: [unnecessary-lambda] +_ = lambda: list() # replaceable with "list" +# +1: [unnecessary-lambda] +_ = lambda x: hash(x) # replaceable with "hash" +# +1: [unnecessary-lambda] +_ = lambda x, y: min(x, y) # replaceable with "min" + +# A function that can take any arguments given to it. +_ANYARGS = lambda *args, **kwargs: 'completely arbitrary return value' + +# Some more complex forms of unnecessary lambda expressions. +# +1: [unnecessary-lambda] +_ = lambda *args: _ANYARGS(*args) +# +1: [unnecessary-lambda] +_ = lambda **kwargs: _ANYARGS(**kwargs) +# +1: [unnecessary-lambda] +_ = lambda *args, **kwargs: _ANYARGS(*args, **kwargs) +# +1: [unnecessary-lambda] +_ = lambda x, y, z, *args, **kwargs: _ANYARGS(x, y, z, *args, **kwargs) + +# Lambdas that are *not* unnecessary and should *not* trigger warnings. +_ = lambda x: x +_ = lambda x: x() +_ = lambda x=4: hash(x) +_ = lambda x, y: list(range(y, x)) +_ = lambda x: list(range(5, x)) +_ = lambda x, y: list(range(x, 5)) +_ = lambda x, y, z: x.y(z) +_ = lambda: 5 +_ = lambda **kwargs: _ANYARGS() +_ = lambda **kwargs: _ANYARGS(**dict([('three', 3)])) +_ = lambda **kwargs: _ANYARGS(**{'three': 3}) +_ = lambda dict_arg, **kwargs: _ANYARGS(kwargs, **dict_arg) +_ = lambda *args: _ANYARGS() +_ = lambda *args: _ANYARGS(*list([3, 4])) +_ = lambda *args: _ANYARGS(*[3, 4]) +_ = lambda list_arg, *args: _ANYARGS(args, *list_arg) +_ = lambda: _ANYARGS(*[3]) +_ = lambda: _ANYARGS(**{'three': 3}) +_ = lambda: _ANYARGS(*[3], **{'three': 3}) + +# Don't warn about this. +_ = lambda: code().analysis() diff --git a/test/functional/unnecessary_lambda.txt b/test/functional/unnecessary_lambda.txt new file mode 100644 index 0000000..de13882 --- /dev/null +++ b/test/functional/unnecessary_lambda.txt @@ -0,0 +1,7 @@ +unnecessary-lambda:9::Lambda may not be necessary +unnecessary-lambda:11::Lambda may not be necessary +unnecessary-lambda:13::Lambda may not be necessary +unnecessary-lambda:20::Lambda may not be necessary +unnecessary-lambda:22::Lambda may not be necessary +unnecessary-lambda:24::Lambda may not be necessary +unnecessary-lambda:26::Lambda may not be necessary diff --git a/test/input/func_w0108.py b/test/input/func_w0108.py deleted file mode 100644 index 0d4cc62..0000000 --- a/test/input/func_w0108.py +++ /dev/null @@ -1,40 +0,0 @@ -# pylint: disable=W0142 -"""test suspicious lambda expressions -""" - -__revision__ = '' - -# Some simple examples of the most commonly encountered forms. -_ = lambda: list() # replaceable with "list" -_ = lambda x: hash(x) # replaceable with "hash" -_ = lambda x, y: min(x, y) # replaceable with "min" - -# A function that can take any arguments given to it. -_ANYARGS = lambda *args, **kwargs: 'completely arbitrary return value' - -# Some more complex forms of unnecessary lambda expressions. -_ = lambda *args: _ANYARGS(*args) -_ = lambda **kwargs: _ANYARGS(**kwargs) -_ = lambda *args, **kwargs: _ANYARGS(*args, **kwargs) -_ = lambda x, y, z, *args, **kwargs: _ANYARGS(x, y, z, *args, **kwargs) - -# Lambdas that are *not* unnecessary and should *not* trigger warnings. -_ = lambda x: x -_ = lambda x: x() -_ = lambda x=4: hash(x) -_ = lambda x, y: range(y, x) -_ = lambda x: range(5, x) -_ = lambda x, y: range(x, 5) -_ = lambda x, y, z: x.y(z) -_ = lambda: 5 -_ = lambda **kwargs: _ANYARGS() -_ = lambda **kwargs: _ANYARGS(**dict([('three', 3)])) -_ = lambda **kwargs: _ANYARGS(**{'three': 3}) -_ = lambda dict_arg, **kwargs: _ANYARGS(kwargs, **dict_arg) -_ = lambda *args: _ANYARGS() -_ = lambda *args: _ANYARGS(*list([3, 4])) -_ = lambda *args: _ANYARGS(*[3, 4]) -_ = lambda list_arg, *args: _ANYARGS(args, *list_arg) -_ = lambda: _ANYARGS(*[3]) -_ = lambda: _ANYARGS(**{'three': 3}) -_ = lambda: _ANYARGS(*[3], **{'three': 3}) diff --git a/test/messages/func_w0108.txt b/test/messages/func_w0108.txt deleted file mode 100644 index 70e0925..0000000 --- a/test/messages/func_w0108.txt +++ /dev/null @@ -1,8 +0,0 @@ -W: 8:: Lambda may not be necessary -W: 9:: Lambda may not be necessary -W: 10:: Lambda may not be necessary -W: 16:: Lambda may not be necessary -W: 17:: Lambda may not be necessary -W: 18:: Lambda may not be necessary -W: 19:: Lambda may not be necessary - -- cgit v1.2.1 From 331732cc5019f19a323e1176b58c1d8c82c5ced4 Mon Sep 17 00:00:00 2001 From: cpopa Date: Fri, 25 Jul 2014 17:18:47 +0200 Subject: W0108 was ported. --- test/test_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_func.py b/test/test_func.py index c6394a1..51b6ba3 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -45,7 +45,7 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310', 'E0202', - 'W0711']) + 'W0711', 'W0108']) @testlib.tag('coverage') def test_exhaustivity(self): -- cgit v1.2.1 From 94809f51eb4bc1d46e573be6763271f05558b80a Mon Sep 17 00:00:00 2001 From: Sylvain Th?nault Date: Fri, 25 Jul 2014 17:23:37 +0200 Subject: linting pylint --- checkers/base.py | 407 ++++++++++++++++++++++---------------------- checkers/classes.py | 80 +++++---- checkers/design_analysis.py | 22 +-- checkers/exceptions.py | 12 +- checkers/format.py | 94 +++++----- checkers/imports.py | 18 +- checkers/logging.py | 42 ++--- checkers/newstyle.py | 4 +- checkers/similar.py | 10 +- checkers/stdlib.py | 7 +- checkers/strings.py | 11 +- checkers/typecheck.py | 31 ++-- checkers/utils.py | 29 ++-- checkers/variables.py | 41 ++--- epylint.py | 2 +- gui.py | 33 ++-- lint.py | 120 ++++++------- pyreverse/main.py | 73 ++++---- pyreverse/writer.py | 6 +- setup.py | 18 +- testutils.py | 8 +- utils.py | 6 +- 22 files changed, 534 insertions(+), 540 deletions(-) diff --git a/checkers/base.py b/checkers/base.py index 1a41f59..fb63c51 100644 --- a/checkers/base.py +++ b/checkers/base.py @@ -124,8 +124,8 @@ def _determine_function_name_type(node): # If the function is a property (decorated with @property # or @abc.abstractproperty), the name type is 'attr'. if (isinstance(decorator, astroid.Name) or - (isinstance(decorator, astroid.Getattr) and - decorator.attrname == 'abstractproperty')): + (isinstance(decorator, astroid.Getattr) and + decorator.attrname == 'abstractproperty')): infered = safe_infer(decorator) if infered and infered.qname() in PROPERTY_CLASSES: return 'attr' @@ -208,7 +208,7 @@ def redefined_by_decorator(node): if node.decorators: for decorator in node.decorators.nodes: if (isinstance(decorator, astroid.Getattr) and - getattr(decorator.expr, 'name', None) == node.name): + getattr(decorator.expr, 'name', None) == node.name): return True return False @@ -218,57 +218,53 @@ class _BasicChecker(BaseChecker): class BasicErrorChecker(_BasicChecker): msgs = { - 'E0100': ('__init__ method is a generator', - 'init-is-generator', - 'Used when the special class method __init__ is turned into a ' - 'generator by a yield in its body.'), - 'E0101': ('Explicit return in __init__', - 'return-in-init', - 'Used when the special class method __init__ has an explicit \ - return value.'), - 'E0102': ('%s already defined line %s', - 'function-redefined', - 'Used when a function / class / method is redefined.'), - 'E0103': ('%r not properly in loop', - 'not-in-loop', - 'Used when break or continue keywords are used outside a loop.'), - - 'E0104': ('Return outside function', - 'return-outside-function', - 'Used when a "return" statement is found outside a function or ' - 'method.'), - 'E0105': ('Yield outside function', - 'yield-outside-function', - 'Used when a "yield" statement is found outside a function or ' - 'method.'), - 'E0106': ('Return with argument inside generator', - '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).', - {'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" - "pre-decrement operator -- and ++, which doesn't exist in Python."), - 'E0108': ('Duplicate argument name %s in function definition', - 'duplicate-argument-name', - 'Duplicate argument names in function definitions are syntax' - ' errors.'), - '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 ' - 'with a break statement, otherwise the statements under else ' - 'should be on the same scope as the loop itself.'), - } - - def __init__(self, linter): - _BasicChecker.__init__(self, linter) + 'E0100': ('__init__ method is a generator', + 'init-is-generator', + 'Used when the special class method __init__ is turned into a ' + 'generator by a yield in its body.'), + 'E0101': ('Explicit return in __init__', + 'return-in-init', + 'Used when the special class method __init__ has an explicit ' + 'return value.'), + 'E0102': ('%s already defined line %s', + 'function-redefined', + 'Used when a function / class / method is redefined.'), + 'E0103': ('%r not properly in loop', + 'not-in-loop', + 'Used when break or continue keywords are used outside a loop.'), + 'E0104': ('Return outside function', + 'return-outside-function', + 'Used when a "return" statement is found outside a function or ' + 'method.'), + 'E0105': ('Yield outside function', + 'yield-outside-function', + 'Used when a "yield" statement is found outside a function or ' + 'method.'), + 'E0106': ('Return with argument inside generator', + '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).', + {'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" + "pre-decrement operator -- and ++, which doesn't exist in Python."), + 'E0108': ('Duplicate argument name %s in function definition', + 'duplicate-argument-name', + 'Duplicate argument names in function definitions are syntax' + ' errors.'), + '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 ' + 'with a break statement, otherwise the statements under else ' + 'should be on the same scope as the loop itself.'), + } @check_messages('function-redefined') def visit_class(self, node): @@ -289,11 +285,11 @@ class BasicErrorChecker(_BasicChecker): else: values = [r.value for r in returns] # Are we returning anything but None from constructors - if [v for v in values if - not (v is None or - (isinstance(v, astroid.Const) and v.value is None) or - (isinstance(v, astroid.Name) and v.name == 'None') - )]: + if [v for v in values + if not (v is None or + (isinstance(v, astroid.Const) and v.value is None) or + (isinstance(v, astroid.Name) and v.name == 'None') + )]: self.add_message('return-in-init', node=node) elif node.is_generator(): # make sure we don't mix non-None returns and yields @@ -342,8 +338,8 @@ class BasicErrorChecker(_BasicChecker): def visit_unaryop(self, node): """check use of the non-existent ++ and -- operator operator""" if ((node.op in '+-') and - isinstance(node.operand, astroid.UnaryOp) and - (node.operand.op == node.op)): + isinstance(node.operand, astroid.UnaryOp) and + (node.operand.op == node.op)): self.add_message('nonexistent-operator', node=node, args=node.op*2) @check_messages('abstract-class-instantiated') @@ -364,14 +360,12 @@ class BasicErrorChecker(_BasicChecker): # by ClassNode.metaclass() for ancestor in infered.ancestors(): if (ancestor.qname() == 'abc.ABC' and - has_abstract_methods(infered)): - + has_abstract_methods(infered)): self.add_message('abstract-class-instantiated', node=node) break return if (metaclass.qname() == 'abc.ABCMeta' and - has_abstract_methods(infered)): - + has_abstract_methods(infered)): self.add_message('abstract-class-instantiated', node=node) def _check_else_on_loop(self, node): @@ -417,88 +411,88 @@ functions, methods name = 'basic' msgs = { - 'W0101': ('Unreachable code', - 'unreachable', - 'Used when there is some code behind a "return" or "raise" \ - statement, which will never be accessed.'), - 'W0102': ('Dangerous default value %s as argument', - 'dangerous-default-value', - 'Used when a mutable value as list or dictionary is detected in \ - a default value for an argument.'), - 'W0104': ('Statement seems to have no effect', - 'pointless-statement', - 'Used when a statement doesn\'t have (or at least seems to) \ - any effect.'), - 'W0105': ('String statement has no effect', - 'pointless-string-statement', - 'Used when a string is used as a statement (which of course \ - has no effect). This is a particular case of W0104 with its \ - own message so you can easily disable it if you\'re using \ - those strings as documentation, instead of comments.'), - 'W0106': ('Expression "%s" is assigned to nothing', - 'expression-not-assigned', - 'Used when an expression that is not a function call is assigned\ - to nothing. Probably something else was intended.'), - 'W0108': ('Lambda may not be necessary', - 'unnecessary-lambda', - 'Used when the body of a lambda expression is a function call \ - on the same argument list as the lambda itself; such lambda \ - expressions are in all but a few cases replaceable with the \ - function being called in the body of the lambda.'), - 'W0109': ("Duplicate key %r in dictionary", - 'duplicate-key', - "Used when a dictionary expression binds the same key multiple \ - times."), - 'W0122': ('Use of exec', - 'exec-used', - 'Used when you use the "exec" statement (function for Python 3), to discourage its \ - usage. That doesn\'t mean you can not use it !'), - '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 ' - 'bad-function option). Usual black listed functions are the ones ' - 'like map, or filter , where Python offers now some cleaner ' - 'alternative like list comprehension.'), - 'W0142': ('Used * or ** magic', - 'star-args', - 'Used when a function or method is called using `*args` or ' - '`**kwargs` to dispatch arguments. This doesn\'t improve ' - 'readability and should be used with care.'), - 'W0150': ("%s statement in finally block may swallow exception", - 'lost-exception', - "Used when a break or a return statement is found inside the \ - finally clause of a try...finally block: the exceptions raised \ - in the try clause will be silently swallowed instead of being \ - re-raised."), - 'W0199': ('Assert called on a 2-uple. Did you mean \'assert x,y\'?', - 'assert-on-tuple', - 'A call of assert on a tuple will always evaluate to true if ' - 'the tuple is not empty, and will always evaluate to false if ' - 'it is.'), - 'W0121': ('Use raise ErrorClass(args) instead of raise ErrorClass, args.', - 'old-raise-syntax', - "Used when the alternate raise syntax 'raise foo, bar' is used " - "instead of 'raise foo(bar)'.", - {'maxversion': (3, 0)}), - - 'C0121': ('Missing required attribute "%s"', # W0103 - 'missing-module-attribute', - 'Used when an attribute required for modules is missing.'), - - 'E0109': ('Missing argument to reversed()', - 'missing-reversed-argument', - 'Used when reversed() builtin didn\'t receive an argument.'), - 'E0111': ('The first reversed() argument is not a sequence', - 'bad-reversed-sequence', - 'Used when the first argument to reversed() builtin ' - 'isn\'t a sequence (does not implement __reversed__, ' - 'nor __getitem__ and __len__'), + 'W0101': ('Unreachable code', + 'unreachable', + 'Used when there is some code behind a "return" or "raise" ' + 'statement, which will never be accessed.'), + 'W0102': ('Dangerous default value %s as argument', + 'dangerous-default-value', + 'Used when a mutable value as list or dictionary is detected in ' + 'a default value for an argument.'), + 'W0104': ('Statement seems to have no effect', + 'pointless-statement', + 'Used when a statement doesn\'t have (or at least seems to) ' + 'any effect.'), + 'W0105': ('String statement has no effect', + 'pointless-string-statement', + 'Used when a string is used as a statement (which of course ' + 'has no effect). This is a particular case of W0104 with its ' + 'own message so you can easily disable it if you\'re using ' + 'those strings as documentation, instead of comments.'), + 'W0106': ('Expression "%s" is assigned to nothing', + 'expression-not-assigned', + 'Used when an expression that is not a function call is assigned ' + 'to nothing. Probably something else was intended.'), + 'W0108': ('Lambda may not be necessary', + 'unnecessary-lambda', + 'Used when the body of a lambda expression is a function call ' + 'on the same argument list as the lambda itself; such lambda ' + 'expressions are in all but a few cases replaceable with the ' + 'function being called in the body of the lambda.'), + 'W0109': ("Duplicate key %r in dictionary", + 'duplicate-key', + 'Used when a dictionary expression binds the same key multiple ' + 'times.'), + 'W0122': ('Use of exec', + 'exec-used', + 'Used when you use the "exec" statement (function for Python 3), to discourage its ' + 'usage. That doesn\'t mean you can not use it !'), + '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 ' + 'bad-function option). Usual black listed functions are the ones ' + 'like map, or filter , where Python offers now some cleaner ' + 'alternative like list comprehension.'), + 'W0142': ('Used * or ** magic', + 'star-args', + 'Used when a function or method is called using `*args` or ' + '`**kwargs` to dispatch arguments. This doesn\'t improve ' + 'readability and should be used with care.'), + 'W0150': ("%s statement in finally block may swallow exception", + 'lost-exception', + 'Used when a break or a return statement is found inside the ' + 'finally clause of a try...finally block: the exceptions raised ' + 'in the try clause will be silently swallowed instead of being ' + 're-raised.'), + 'W0199': ('Assert called on a 2-uple. Did you mean \'assert x,y\'?', + 'assert-on-tuple', + 'A call of assert on a tuple will always evaluate to true if ' + 'the tuple is not empty, and will always evaluate to false if ' + 'it is.'), + 'W0121': ('Use raise ErrorClass(args) instead of raise ErrorClass, args.', + 'old-raise-syntax', + "Used when the alternate raise syntax 'raise foo, bar' is used " + "instead of 'raise foo(bar)'.", + {'maxversion': (3, 0)}), + + 'C0121': ('Missing required attribute "%s"', # W0103 + 'missing-module-attribute', + 'Used when an attribute required for modules is missing.'), + + 'E0109': ('Missing argument to reversed()', + 'missing-reversed-argument', + 'Used when reversed() builtin didn\'t receive an argument.'), + 'E0111': ('The first reversed() argument is not a sequence', + 'bad-reversed-sequence', + 'Used when the first argument to reversed() builtin ' + 'isn\'t a sequence (does not implement __reversed__, ' + 'nor __getitem__ and __len__'), } @@ -507,14 +501,14 @@ functions, methods 'metavar' : '', 'help' : 'Required attributes for module, separated by a ' 'comma'} - ), + ), ('bad-functions', {'default' : BAD_FUNCTIONS, 'type' :'csv', 'metavar' : '', 'help' : 'List of builtins function names that should not be ' 'used, separated by a comma'} - ), - ) + ), + ) reports = (('RP0101', 'Statistics by type', report_by_type_stats),) def __init__(self, linter): @@ -549,7 +543,7 @@ functions, methods """check for various kind of statements without effect""" expr = node.value if isinstance(expr, astroid.Const) and isinstance(expr.value, - basestring): + basestring): # treat string statement in a separated message # Handle PEP-257 attribute docstrings. # An attribute docstring is defined as being a string right after @@ -561,7 +555,7 @@ functions, methods else: sibling = expr.previous_sibling() if (sibling.scope() is scope and - isinstance(sibling, astroid.Assign)): + isinstance(sibling, astroid.Assign)): return self.add_message('pointless-string-statement', node=node) return @@ -572,11 +566,12 @@ functions, methods # warn W0106 if we have any underlying function call (we can't predict # side effects), else pointless-statement if (isinstance(expr, (astroid.Yield, astroid.CallFunc)) or - (isinstance(node.parent, astroid.TryExcept) and - node.parent.body == [node])): + (isinstance(node.parent, astroid.TryExcept) and + node.parent.body == [node])): return if any(expr.nodes_of_class(astroid.CallFunc)): - self.add_message('expression-not-assigned', node=node, args=expr.as_string()) + self.add_message('expression-not-assigned', node=node, + args=expr.as_string()) else: self.add_message('pointless-statement', node=node) @@ -609,15 +604,15 @@ functions, methods ordinary_args = list(node.args.args) if node.args.kwarg: if (not call.kwargs - or not isinstance(call.kwargs, astroid.Name) - or node.args.kwarg != call.kwargs.name): + or not isinstance(call.kwargs, astroid.Name) + or node.args.kwarg != call.kwargs.name): return elif call.kwargs: return if node.args.vararg: if (not call.starargs - or not isinstance(call.starargs, astroid.Name) - or node.args.vararg != call.starargs.name): + or not isinstance(call.starargs, astroid.Name) + or node.args.vararg != call.starargs.name): return elif call.starargs: return @@ -651,7 +646,7 @@ functions, methods continue builtins = astroid.bases.BUILTINS if (isinstance(value, astroid.Instance) and - value.qname() in ['.'.join([builtins, x]) for x in ('set', 'dict', 'list')]): + value.qname() in ['.'.join([builtins, x]) for x in ('set', 'dict', 'list')]): if value is default: msg = default.as_string() elif type(value) is astroid.Instance: @@ -707,7 +702,7 @@ functions, methods self.add_message('exec-used', node=node) @check_messages('bad-builtin', 'star-args', 'eval-used', - 'exec-used', 'missing-reversed-argument', + 'exec-used', 'missing-reversed-argument', 'bad-reversed-sequence') def visit_callfunc(self, node): """visit a CallFunc node -> check if this is not a blacklisted builtin @@ -744,7 +739,7 @@ functions, methods def visit_assert(self, node): """check the use of an assert statement on a tuple.""" if node.fail is None and isinstance(node.test, astroid.Tuple) and \ - len(node.test.elts) == 2: + len(node.test.elts) == 2: self.add_message('assert-on-tuple', node=node) @check_messages('duplicate-key') @@ -789,7 +784,7 @@ functions, methods return _node = _parent _parent = _node.parent - + def _check_reversed(self, node): """ check that the argument to `reversed` is a sequence """ try: @@ -808,17 +803,17 @@ functions, methods except InferenceError: return if (getattr(func, 'name', None) == 'iter' and - is_builtin_object(func)): + is_builtin_object(func)): self.add_message('bad-reversed-sequence', node=node) return if isinstance(argument, astroid.Instance): - if (argument._proxied.name == 'dict' and - is_builtin_object(argument._proxied)): - self.add_message('bad-reversed-sequence', node=node) - return + if (argument._proxied.name == 'dict' and + is_builtin_object(argument._proxied)): + self.add_message('bad-reversed-sequence', node=node) + return elif any(ancestor.name == 'dict' and is_builtin_object(ancestor) - for ancestor in argument._proxied.ancestors()): + for ancestor in argument._proxied.ancestors()): # mappings aren't accepted by reversed() self.add_message('bad-reversed-sequence', node=node) return @@ -831,10 +826,10 @@ functions, methods break else: break - else: + else: # check if it is a .deque. It doesn't seem that - # we can retrieve special methods - # from C implemented constructs + # we can retrieve special methods + # from C implemented constructs if argument._proxied.qname().endswith(".deque"): return self.add_message('bad-reversed-sequence', node=node) @@ -860,53 +855,51 @@ def _create_naming_options(): for name_type, (rgx, human_readable_name) in _NAME_TYPES.iteritems(): name_type = name_type.replace('_', '-') name_options.append(( - '%s-rgx' % (name_type,), + '%s-rgx' % (name_type,), {'default': rgx, 'type': 'regexp', 'metavar': '', 'help': 'Regular expression matching correct %s names' % (human_readable_name,)})) name_options.append(( - '%s-name-hint' % (name_type,), + '%s-name-hint' % (name_type,), {'default': rgx.pattern, 'type': 'string', 'metavar': '', 'help': 'Naming hint for %s names' % (human_readable_name,)})) - - return tuple(name_options) + 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"%s', - 'invalid-name', - 'Used when the name doesn\'t match the regular expression \ - associated to its type (constant, variable, class...).'), + 'C0102': ('Black listed name "%s"', + 'blacklisted-name', + 'Used when the name is listed in the black list (unauthorized ' + 'names).'), + '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 = (# XXX use set - ('good-names', + options = (('good-names', {'default' : ('i', 'j', 'k', 'ex', 'Run', '_'), 'type' :'csv', 'metavar' : '', 'help' : 'Good variable names which should always be accepted,' ' separated by a comma'} - ), + ), ('bad-names', {'default' : ('foo', 'bar', 'baz', 'toto', 'tutu', 'tata'), 'type' :'csv', 'metavar' : '', 'help' : 'Bad variable names which should always be refused, ' 'separated by a comma'} - ), + ), ('name-group', {'default' : (), 'type' :'csv', 'metavar' : '', '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' : '', 'help': 'Include a hint for the correct naming format with invalid-name'} - ), - ) + _create_naming_options() + ), + ) + _create_naming_options() def __init__(self, linter): @@ -1028,30 +1021,30 @@ class NameChecker(_BasicChecker): class DocStringChecker(_BasicChecker): msgs = { - 'C0111': ('Missing %s docstring', # W0131 - 'missing-docstring', - 'Used when a module, function, class or method has no docstring.\ - Some special methods like __init__ doesn\'t necessary require a \ - docstring.'), - 'C0112': ('Empty %s docstring', # W0132 - 'empty-docstring', - 'Used when a module, function, class or method has an empty \ - docstring (it would be too easy ;).'), - } + 'C0111': ('Missing %s docstring', # W0131 + 'missing-docstring', + 'Used when a module, function, class or method has no docstring.' + 'Some special methods like __init__ doesn\'t necessary require a ' + 'docstring.'), + 'C0112': ('Empty %s docstring', # W0132 + 'empty-docstring', + 'Used when a module, function, class or method has an empty ' + 'docstring (it would be too easy ;).'), + } options = (('no-docstring-rgx', {'default' : NO_REQUIRED_DOC_RGX, 'type' : 'regexp', 'metavar' : '', 'help' : 'Regular expression which should only match ' 'function or class names that do not require a ' 'docstring.'} - ), + ), ('docstring-min-length', {'default' : -1, 'type' : 'int', 'metavar' : '', 'help': ('Minimum line length for functions/classes that' ' require docstrings, shorter ones are exempt.')} - ), - ) + ), + ) def open(self): @@ -1111,7 +1104,7 @@ class PassChecker(_BasicChecker): 'unnecessary-pass', 'Used when a "pass" statement that can be avoided is ' 'encountered.'), - } + } @check_messages('unnecessary-pass') def visit_pass(self, node): if len(node.parent.child_sequence(node)) > 1: @@ -1131,7 +1124,7 @@ class LambdaForComprehensionChecker(_BasicChecker): '"filter". It could be clearer as a list ' 'comprehension or generator expression.', {'maxversion': (3, 0)}), - } + } @check_messages('deprecated-lambda') def visit_callfunc(self, node): @@ -1144,7 +1137,7 @@ class LambdaForComprehensionChecker(_BasicChecker): return infered = safe_infer(node.func) if (is_builtin_object(infered) - and infered.name in ['map', 'filter']): + and infered.name in ['map', 'filter']): self.add_message('deprecated-lambda', node=node) diff --git a/checkers/classes.py b/checkers/classes.py index 170bb19..61769f2 100644 --- a/checkers/classes.py +++ b/checkers/classes.py @@ -25,9 +25,9 @@ from astroid.bases import Generator from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker -from pylint.checkers.utils import (PYMETHODS, overrides_a_method, - check_messages, is_attr_private, is_attr_protected, node_frame_class, - safe_infer) +from pylint.checkers.utils import ( + PYMETHODS, overrides_a_method, check_messages, is_attr_private, + is_attr_protected, node_frame_class, safe_infer) if sys.version_info >= (3, 0): NEXT_METHOD = '__next__' @@ -106,12 +106,12 @@ MSGS = { 'Used when a static method has "self" or a value specified in ' 'valid-classmethod-first-arg option or ' 'valid-metaclass-classmethod-first-arg option as first argument.' - ), + ), 'R0201': ('Method could be a function', 'no-self-use', 'Used when a method doesn\'t use its bound instance, and so could\ be written as a function.' - ), + ), 'E0221': ('Interface resolved to %s is not a class', 'interface-is-not-class', @@ -133,7 +133,7 @@ MSGS = { 'abstract-method', 'Used when an abstract method (i.e. raise NotImplementedError) is \ not overridden in concrete class.' - ), + ), 'F0220': ('failed to resolve interfaces implemented by %s (%s)', # W0224 'unresolved-interface', 'Used when a PyLint as failed to find interfaces implemented by \ @@ -198,45 +198,43 @@ class ClassChecker(BaseChecker): # configuration options options = (('ignore-iface-methods', {'default' : (#zope interface - 'isImplementedBy', 'deferred', 'extends', 'names', - 'namesAndDescriptions', 'queryDescriptionFor', 'getBases', - 'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue', - 'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue', - 'isImplementedByInstancesOf', - # twisted - 'adaptWith', - # logilab.common interface - 'is_implemented_by'), + 'isImplementedBy', 'deferred', 'extends', 'names', + 'namesAndDescriptions', 'queryDescriptionFor', 'getBases', + 'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue', + 'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue', + 'isImplementedByInstancesOf', + # twisted + 'adaptWith', + # logilab.common interface + 'is_implemented_by'), 'type' : 'csv', 'metavar' : '', 'help' : 'List of interface methods to ignore, \ separated by a comma. This is used for instance to not check methods defines \ in Zope\'s Interface base class.'} - ), - + ), ('defining-attr-methods', {'default' : ('__init__', '__new__', 'setUp'), 'type' : 'csv', 'metavar' : '', 'help' : 'List of method names used to declare (i.e. assign) \ instance attributes.'} - ), + ), ('valid-classmethod-first-arg', {'default' : ('cls',), 'type' : 'csv', 'metavar' : '', 'help' : 'List of valid names for the first argument in \ a class method.'} - ), + ), ('valid-metaclass-classmethod-first-arg', {'default' : ('mcs',), 'type' : 'csv', 'metavar' : '', 'help' : 'List of valid names for the first argument in \ a metaclass class method.'} - ), - - ) + ), + ) def __init__(self, linter=None): BaseChecker.__init__(self, linter) @@ -400,7 +398,7 @@ a metaclass class method.'} if infered is YES: continue if (not isinstance(infered, astroid.Const) or - not isinstance(infered.value, str)): + not isinstance(infered.value, str)): self.add_message('invalid-slots-object', args=infered.as_string(), node=elt) @@ -418,7 +416,7 @@ a metaclass class method.'} for infered_node in infered: if (infered_node is YES - or isinstance(infered_node, Generator)): + or isinstance(infered_node, Generator)): continue if isinstance(infered_node, astroid.Instance): try: @@ -451,10 +449,10 @@ a metaclass class method.'} return class_node = node.parent.frame() if (self._meth_could_be_func and node.type == 'method' - and not node.name in PYMETHODS - and not (node.is_abstract() or - overrides_a_method(class_node, node.name)) - and class_node.type != 'interface'): + and not node.name in PYMETHODS + and not (node.is_abstract() or + overrides_a_method(class_node, node.name)) + and class_node.type != 'interface'): self.add_message('no-self-use', node=node) def visit_getattr(self, node): @@ -629,8 +627,8 @@ a metaclass class method.'} # static method if node.type == 'staticmethod': if (first_arg == 'self' or - first_arg in self.config.valid_classmethod_first_arg or - first_arg in self.config.valid_metaclass_classmethod_first_arg): + first_arg in self.config.valid_classmethod_first_arg or + first_arg in self.config.valid_metaclass_classmethod_first_arg): self.add_message('bad-staticmethod-argument', args=first, node=node) return self._first_attrs[-1] = None @@ -641,19 +639,22 @@ a metaclass class method.'} elif metaclass: # metaclass __new__ or classmethod if node.type == 'classmethod': - self._check_first_arg_config(first, + self._check_first_arg_config( + first, self.config.valid_metaclass_classmethod_first_arg, node, 'bad-mcs-classmethod-argument', node.name) # metaclass regular method else: - self._check_first_arg_config(first, + self._check_first_arg_config( + first, self.config.valid_classmethod_first_arg, node, 'bad-mcs-method-argument', node.name) # regular class else: # class method if node.type == 'classmethod': - self._check_first_arg_config(first, + self._check_first_arg_config( + first, self.config.valid_classmethod_first_arg, node, 'bad-classmethod-argument', node.name) # regular method without self as argument @@ -666,11 +667,8 @@ a metaclass class method.'} if len(config) == 1: valid = repr(config[0]) else: - valid = ', '.join( - repr(v) - for v in config[:-1]) - valid = '%s or %r' % ( - valid, config[-1]) + valid = ', '.join(repr(v) for v in config[:-1]) + valid = '%s or %r' % (valid, config[-1]) self.add_message(message, args=(method_name, valid), node=node) def _check_bases_classes(self, node): @@ -727,7 +725,7 @@ a metaclass class method.'} continue # check signature self._check_signature(method, imethod, - '%s interface' % iface.name) + '%s interface' % iface.name) except astroid.InferenceError: if e0221_hack[0]: return @@ -746,7 +744,7 @@ a metaclass class method.'} method """ if (not self.linter.is_message_enabled('super-init-not-called') and - not self.linter.is_message_enabled('non-parent-init-called')): + not self.linter.is_message_enabled('non-parent-init-called')): return klass_node = node.parent.frame() to_call = _ancestors_to_call(klass_node) @@ -758,7 +756,7 @@ a metaclass class method.'} continue # skip the test if using super if isinstance(expr.expr, astroid.CallFunc) and \ - isinstance(expr.expr.func, astroid.Name) and \ + isinstance(expr.expr.func, astroid.Name) and \ expr.expr.func.name == 'super': return try: diff --git a/checkers/design_analysis.py b/checkers/design_analysis.py index c9ef4df..0725ccf 100644 --- a/checkers/design_analysis.py +++ b/checkers/design_analysis.py @@ -105,59 +105,59 @@ class MisdesignChecker(BaseChecker): options = (('max-args', {'default' : 5, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of arguments for function / method'} - ), + ), ('ignored-argument-names', {'default' : IGNORED_ARGUMENT_NAMES, 'type' :'regexp', 'metavar' : '', 'help' : 'Argument names that match this expression will be ' 'ignored. Default to name with leading underscore'} - ), + ), ('max-locals', {'default' : 15, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of locals for function / method body'} - ), + ), ('max-returns', {'default' : 6, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of return / yield for function / ' 'method body'} - ), + ), ('max-branches', {'default' : 12, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of branch for function / method body'} - ), + ), ('max-statements', {'default' : 50, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of statements in function / method ' 'body'} - ), + ), ('max-parents', {'default' : 7, 'type' : 'int', 'metavar' : '', 'help' : 'Maximum number of parents for a class (see R0901).'} - ), + ), ('max-attributes', {'default' : 7, 'type' : 'int', 'metavar' : '', 'help' : 'Maximum number of attributes for a class \ (see R0902).'} - ), + ), ('min-public-methods', {'default' : 2, 'type' : 'int', 'metavar' : '', 'help' : 'Minimum number of public methods for a class \ (see R0903).'} - ), + ), ('max-public-methods', {'default' : 20, 'type' : 'int', 'metavar' : '', 'help' : 'Maximum number of public methods for a class \ (see R0904).'} - ), - ) + ), + ) def __init__(self, linter=None): BaseChecker.__init__(self, linter) diff --git a/checkers/exceptions.py b/checkers/exceptions.py index c91c95d..94186eb 100644 --- a/checkers/exceptions.py +++ b/checkers/exceptions.py @@ -137,8 +137,8 @@ class ExceptionsChecker(BaseChecker): 'help' : 'Exceptions that will emit a warning ' 'when being caught. Defaults to "%s"' % ( ', '.join(OVERGENERAL_EXCEPTIONS),)} - ), - ) + ), + ) @check_messages('raising-string', 'nonstandard-exception', 'raising-bad-type', 'raising-non-exception', 'notimplemented-raised', 'bad-exception-context') @@ -162,7 +162,7 @@ class ExceptionsChecker(BaseChecker): elif (not isinstance(cause, astroid.Class) and not inherit_from_std_ex(cause)): self.add_message('bad-exception-context', - node=node) + node=node) expr = node.exc if self._check_raise_value(node, expr): return @@ -269,12 +269,12 @@ class ExceptionsChecker(BaseChecker): previous_exc.name, exc.name) 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 not is_raising(handler.body)): + and exc.root().name == EXCEPTIONS_MODULE + and not is_raising(handler.body)): self.add_message('broad-except', args=exc.name, node=handler.type) if (not inherit_from_std_ex(exc) and - exc.root().name != BUILTINS_NAME): + exc.root().name != BUILTINS_NAME): # try to see if the exception is based on a C based # exception, by infering all the base classes and # looking for inference errors diff --git a/checkers/format.py b/checkers/format.py index c07ff32..7b5eb64 100644 --- a/checkers/format.py +++ b/checkers/format.py @@ -68,7 +68,7 @@ MSGS = { 'C0302': ('Too many lines in module (%s)', # was W0302 'too-many-lines', 'Used when a module has too much lines, reducing its readability.' - ), + ), 'C0303': ('Trailing whitespace', 'trailing-whitespace', 'Used when there is whitespace between the end of a line and the ' @@ -111,20 +111,20 @@ MSGS = { if sys.version_info < (3, 0): MSGS.update({ - 'W0331': ('Use of the <> operator', - 'old-ne-operator', - 'Used when the deprecated "<>" operator is used instead \ - of "!=".'), - 'W0332': ('Use of "l" as long integer identifier', - 'lowercase-l-suffix', - 'Used when a lower case "l" is used to mark a long integer. You ' - 'should use a upper case "L" since the letter "l" looks too much ' - 'like the digit "1"'), - 'W0333': ('Use of the `` operator', - 'backtick', - 'Used when the deprecated "``" (backtick) operator is used ' - 'instead of the str() function.', - {'scope': WarningScope.NODE}), + 'W0331': ('Use of the <> operator', + 'old-ne-operator', + 'Used when the deprecated "<>" operator is used instead ' + 'of "!=".'), + 'W0332': ('Use of "l" as long integer identifier', + 'lowercase-l-suffix', + 'Used when a lower case "l" is used to mark a long integer. You ' + 'should use a upper case "L" since the letter "l" looks too much ' + 'like the digit "1"'), + 'W0333': ('Use of the `` operator', + 'backtick', + 'Used when the deprecated "``" (backtick) operator is used ' + 'instead of the str() function.', + {'scope': WarningScope.NODE}), }) @@ -145,29 +145,28 @@ def _column_distance(token1, token2): def _last_token_on_line_is(tokens, line_end, token): - return ( - line_end > 0 and tokens.token(line_end-1) == token or - line_end > 1 and tokens.token(line_end-2) == token - and tokens.type(line_end-1) == tokenize.COMMENT) + return (line_end > 0 and tokens.token(line_end-1) == token or + line_end > 1 and tokens.token(line_end-2) == token + and tokens.type(line_end-1) == tokenize.COMMENT) def _token_followed_by_eol(tokens, position): - return (tokens.type(position+1) == tokenize.NL or - tokens.type(position+1) == tokenize.COMMENT and - tokens.type(position+2) == tokenize.NL) + return (tokens.type(position+1) == tokenize.NL or + tokens.type(position+1) == tokenize.COMMENT and + tokens.type(position+2) == tokenize.NL) def _get_indent_length(line): - """Return the length of the indentation on the given token's line.""" - result = 0 - for char in line: - if char == ' ': - result += 1 - elif char == '\t': - result += _TAB_LENGTH - else: - break - return result + """Return the length of the indentation on the given token's line.""" + result = 0 + for char in line: + if char == ' ': + result += 1 + elif char == '\t': + result += _TAB_LENGTH + else: + break + return result def _get_indent_hint_line(bar_positions, bad_position): @@ -426,9 +425,9 @@ class FormatChecker(BaseTokenChecker): 'help': ('Regexp for a line that is allowed to be longer than ' 'the limit.')}), ('single-line-if-stmt', - {'default': False, 'type' : 'yn', 'metavar' : '', - 'help' : ('Allow the body of an if to be on the same ' - 'line as the test if there is no else.')}), + {'default': False, 'type' : 'yn', 'metavar' : '', + 'help' : ('Allow the body of an if to be on the same ' + 'line as the test if there is no else.')}), ('no-space-check', {'default': ','.join(_NO_SPACE_CHECK_CHOICES), 'type': 'multiple_choice', @@ -438,16 +437,16 @@ class FormatChecker(BaseTokenChecker): ('max-module-lines', {'default' : 1000, 'type' : 'int', 'metavar' : '', 'help': 'Maximum number of lines in a module'} - ), + ), ('indent-string', {'default' : ' ', 'type' : "string", 'metavar' : '', - 'help' : 'String used as indentation unit. This is usually \ -" " (4 spaces) or "\\t" (1 tab).'}), + 'help' : 'String used as indentation unit. This is usually ' + '" " (4 spaces) or "\\t" (1 tab).'}), ('indent-after-paren', {'type': 'int', 'metavar': '', 'default': 4, 'help': 'Number of spaces of indent required inside a hanging ' ' or continued line.'}), - ) + ) def __init__(self, linter=None): BaseTokenChecker.__init__(self, linter) @@ -515,8 +514,9 @@ class FormatChecker(BaseTokenChecker): if not depth: # ')' can't happen after if (foo), since it would be a syntax error. if (tokens[i+1][1] in (':', ')', ']', '}', 'in') or - tokens[i+1][0] in (tokenize.NEWLINE, tokenize.ENDMARKER, - tokenize.COMMENT)): + tokens[i+1][0] in (tokenize.NEWLINE, + tokenize.ENDMARKER, + tokenize.COMMENT)): # The empty tuple () is always accepted. if i == start + 2: return @@ -593,7 +593,7 @@ class FormatChecker(BaseTokenChecker): if self._inside_brackets('['): return if (self._inside_brackets('{') and - _DICT_SEPARATOR in self.config.no_space_check): + _DICT_SEPARATOR in self.config.no_space_check): policy = (_IGNORE, _IGNORE) else: policy = (_MUST_NOT, _MUST) @@ -731,7 +731,7 @@ class FormatChecker(BaseTokenChecker): self.new_line(TokenWrapper(tokens), idx-1, idx+1) else: self.new_line(TokenWrapper(tokens), idx-1, idx) - + if tok_type == tokenize.NEWLINE: # a program statement, or ENDMARKER, will eventually follow, # after some (possibly empty) run of tokens of the form @@ -804,7 +804,7 @@ class FormatChecker(BaseTokenChecker): return state, valid_offsets = self._current_line.get_valid_offsets(next_idx) - # Special handling for hanging comments and strings. If the last line ended + # Special handling for hanging comments and strings. If the last line ended # with a comment (string) and the new line contains only a comment, the line # may also be indented to the start of the previous token. if same_token_around_nl(tokenize.COMMENT) or same_token_around_nl(tokenize.STRING): @@ -844,7 +844,7 @@ class FormatChecker(BaseTokenChecker): # by taking the last line of the body and adding 1, which # should be the line of finally: if (isinstance(node.parent, nodes.TryFinally) - and node in node.parent.finalbody): + and node in node.parent.finalbody): prev_line = node.parent.body[0].tolineno + 1 else: prev_line = node.parent.statement().fromlineno @@ -877,10 +877,10 @@ class FormatChecker(BaseTokenChecker): # For try... except... finally..., the two nodes # appear to be on the same line due to how the AST is built. if (isinstance(node, nodes.TryExcept) and - isinstance(node.parent, nodes.TryFinally)): + isinstance(node.parent, nodes.TryFinally)): return if (isinstance(node.parent, nodes.If) and not node.parent.orelse - and self.config.single_line_if_stmt): + and self.config.single_line_if_stmt): return self.add_message('multiple-statements', node=node) self._visited_lines[line] = 2 diff --git a/checkers/imports.py b/checkers/imports.py index 9ae3935..3242b75 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -48,7 +48,8 @@ def get_first_import(node, context, name, base, level): break elif isinstance(first, astroid.From): if level == first.level and any( - fullname == '%s.%s' % (first.modname, iname[0]) for iname in first.names): + fullname == '%s.%s' % (first.modname, iname[0]) + for iname in first.names): found = True break if found and not are_exclusive(first, node): @@ -178,30 +179,29 @@ class ImportsChecker(BaseChecker): 'metavar' : '', 'help' : 'Deprecated modules which should not be used, \ separated by a comma'} - ), + ), ('import-graph', {'default' : '', 'type' : 'string', 'metavar' : '', 'help' : 'Create a graph of every (i.e. internal and \ external) dependencies in the given file (report RP0402 must not be disabled)'} - ), + ), ('ext-import-graph', {'default' : '', 'type' : 'string', 'metavar' : '', 'help' : 'Create a graph of external dependencies in the \ given file (report RP0402 must not be disabled)'} - ), + ), ('int-import-graph', {'default' : '', 'type' : 'string', 'metavar' : '', 'help' : 'Create a graph of internal dependencies in the \ given file (report RP0402 must not be disabled)'} - ), - - ) + ), + ) def __init__(self, linter=None): BaseChecker.__init__(self, linter) @@ -212,7 +212,7 @@ given file (report RP0402 must not be disabled)'} self.report_external_dependencies), ('RP0402', 'Modules dependencies graph', self.report_dependencies_graph), - ) + ) def open(self): """called before visiting project (i.e set of modules)""" @@ -252,7 +252,7 @@ given file (report RP0402 must not be disabled)'} if prev: # consecutive future statements are possible if not (isinstance(prev, astroid.From) - and prev.modname == '__future__'): + and prev.modname == '__future__'): self.add_message('misplaced-future', node=node) return for name, _ in node.names: diff --git a/checkers/logging.py b/checkers/logging.py index cbdf0f2..d82d74b 100644 --- a/checkers/logging.py +++ b/checkers/logging.py @@ -22,16 +22,16 @@ from pylint.checkers.utils import check_messages MSGS = { 'W1201': ('Specify string format arguments as logging function parameters', - 'logging-not-lazy', - 'Used when a logging statement has a call form of ' - '"logging.(format_string % (format_args...))". ' - 'Such calls should leave string interpolation to the logging ' - 'method itself and be written ' - '"logging.(format_string, format_args...)" ' - 'so that the program may avoid incurring the cost of the ' - 'interpolation in those cases in which no message will be ' - 'logged. For more, see ' - 'http://www.python.org/dev/peps/pep-0282/.'), + 'logging-not-lazy', + 'Used when a logging statement has a call form of ' + '"logging.(format_string % (format_args...))". ' + 'Such calls should leave string interpolation to the logging ' + 'method itself and be written ' + '"logging.(format_string, format_args...)" ' + 'so that the program may avoid incurring the cost of the ' + 'interpolation in those cases in which no message will be ' + 'logged. For more, see ' + 'http://www.python.org/dev/peps/pep-0282/.'), 'E1200': ('Unsupported logging format character %r (%#02x) at index %d', 'logging-unsupported-format', 'Used when an unsupported format character is used in a logging\ @@ -65,10 +65,10 @@ class LoggingChecker(checkers.BaseChecker): {'default' : ('logging',), 'type' : 'csv', 'metavar' : '', - 'help' : ('Logging modules to check that the string format ' - 'arguments are in logging function parameter format')} - ), - ) + '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.""" @@ -105,19 +105,19 @@ class LoggingChecker(checkers.BaseChecker): def visit_callfunc(self, node): """Checks calls to logging methods.""" def is_logging_name(): - return (isinstance(node.func, astroid.Getattr) and - isinstance(node.func.expr, astroid.Name) and - node.func.expr.name in self._logging_names) + return (isinstance(node.func, astroid.Getattr) and + isinstance(node.func.expr, astroid.Name) and + node.func.expr.name in self._logging_names) def is_logger_class(): try: for inferred in node.func.infer(): if isinstance(inferred, astroid.BoundMethod): parent = inferred._proxied.parent - if (isinstance(parent, astroid.Class) and - (parent.qname() == 'logging.Logger' or - any(ancestor.qname() == 'logging.Logger' - for ancestor in parent.ancestors()))): + if (isinstance(parent, astroid.Class) and + (parent.qname() == 'logging.Logger' or + any(ancestor.qname() == 'logging.Logger' + for ancestor in parent.ancestors()))): return True, inferred._proxied.name except astroid.exceptions.InferenceError: pass diff --git a/checkers/newstyle.py b/checkers/newstyle.py index f801c44..cc8f640 100644 --- a/checkers/newstyle.py +++ b/checkers/newstyle.py @@ -89,8 +89,8 @@ class NewStyleConflictChecker(BaseChecker): """check property usage""" parent = node.parent.frame() if (isinstance(parent, astroid.Class) and - not parent.newstyle and - isinstance(node.func, astroid.Name)): + not parent.newstyle and + isinstance(node.func, astroid.Name)): name = node.func.name if name == 'property': self.add_message('property-on-old-class', node=node) diff --git a/checkers/similar.py b/checkers/similar.py index cf671bf..e5c9300 100644 --- a/checkers/similar.py +++ b/checkers/similar.py @@ -107,7 +107,7 @@ class Similar(object): for index2 in find(lineset1[index1]): non_blank = 0 for num, ((_, line1), (_, line2)) in enumerate( - izip(lines1(index1), lines2(index2))): + izip(lines1(index1), lines2(index2))): if line1 != line2: if non_blank > min_lines: yield num, lineset1, index1, lineset2, index2 @@ -249,16 +249,16 @@ class SimilarChecker(BaseChecker, Similar): ('ignore-comments', {'default' : True, 'type' : 'yn', 'metavar' : '', 'help': 'Ignore comments when computing similarities.'} - ), + ), ('ignore-docstrings', {'default' : True, 'type' : 'yn', 'metavar' : '', 'help': 'Ignore docstrings when computing similarities.'} - ), + ), ('ignore-imports', {'default' : False, 'type' : 'yn', 'metavar' : '', 'help': 'Ignore imports when computing similarities.'} - ), - ) + ), + ) # reports reports = (('RP0801', 'Duplication', report_similarities),) diff --git a/checkers/stdlib.py b/checkers/stdlib.py index a1c3133..9913e99 100644 --- a/checkers/stdlib.py +++ b/checkers/stdlib.py @@ -24,7 +24,7 @@ from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker from pylint.checkers import utils -_VALID_OPEN_MODE_REGEX = r'^(r?U|[rwa]\+?b?)$' +_VALID_OPEN_MODE_REGEX = re.compile(r'^(r?U|[rwa]\+?b?)$') if sys.version_info >= (3, 0): OPEN_MODULE = '_io' @@ -58,8 +58,9 @@ class OpenModeChecker(BaseChecker): if mode_arg: mode_arg = utils.safe_infer(mode_arg) if (isinstance(mode_arg, astroid.Const) - and not re.match(_VALID_OPEN_MODE_REGEX, mode_arg.value)): - self.add_message('bad-open-mode', node=node, args=(mode_arg.value)) + and not _VALID_OPEN_MODE_REGEX.match(mode_arg.value)): + self.add_message('bad-open-mode', node=node, + args=(mode_arg.value)) except (utils.NoSuchArgumentError, TypeError): pass diff --git a/checkers/strings.py b/checkers/strings.py index af2ceea..8251a80 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -211,7 +211,7 @@ class StringFormatChecker(BaseChecker): args = node.right if not (isinstance(left, astroid.Const) - and isinstance(left.value, basestring)): + and isinstance(left.value, basestring)): return format_string = left.value try: @@ -298,9 +298,8 @@ class StringMethodsChecker(BaseChecker): def visit_callfunc(self, node): func = utils.safe_infer(node.func) if (isinstance(func, astroid.BoundMethod) - and isinstance(func.bound, astroid.Instance) - and func.bound.name in ('str', 'unicode', 'bytes')): - + and isinstance(func.bound, astroid.Instance) + and func.bound.name in ('str', 'unicode', 'bytes')): if func.name in ('strip', 'lstrip', 'rstrip') and node.args: arg = utils.safe_infer(node.args[0]) if not isinstance(arg, astroid.Const): @@ -413,7 +412,7 @@ class StringMethodsChecker(BaseChecker): previous = previous.getattr(specifier)[0] except astroid.NotFoundError: if (hasattr(previous, 'has_dynamic_getattr') and - previous.has_dynamic_getattr()): + previous.has_dynamic_getattr()): # Don't warn if the object has a custom __getattr__ break path = get_access_path(key, parsed) @@ -504,7 +503,7 @@ class StringConstantChecker(BaseTokenChecker): # No special checks on raw strings at the moment. if 'r' not in prefix: self.process_non_raw_string_token(prefix, string_body, - start_row, start_col) + start_row, start_col) def process_non_raw_string_token(self, prefix, string_body, start_row, start_col): diff --git a/checkers/typecheck.py b/checkers/typecheck.py index 09caa86..fb8f3c6 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -146,39 +146,38 @@ class TypeChecker(BaseChecker): 'help' : 'Tells whether missing members accessed in mixin \ class should be ignored. A mixin class is detected if its name ends with \ "mixin" (case insensitive).'} - ), - ('ignored-modules', - {'default': (), - 'type': 'csv', - 'metavar': '', - 'help': 'List of module names for which member attributes \ + ), + ('ignored-modules', + {'default': (), + 'type': 'csv', + 'metavar': '', + 'help': 'List of module names for which member attributes \ should not be checked (useful for modules/projects where namespaces are \ manipulated during runtime and thus existing member attributes cannot be \ deduced by static analysis'}, - ), + ), ('ignored-classes', {'default' : ('SQLObject',), 'type' : 'csv', 'metavar' : '', 'help' : 'List of classes names for which member attributes \ should not be checked (useful for classes with attributes dynamically set).'} - ), + ), ('zope', {'default' : False, 'type' : 'yn', 'metavar': '', 'help' : 'When zope mode is activated, add a predefined set \ of Zope acquired attributes to generated-members.'} - ), + ), ('generated-members', - {'default' : ( - 'REQUEST', 'acl_users', 'aq_parent'), + {'default' : ('REQUEST', 'acl_users', 'aq_parent'), 'type' : 'string', 'metavar' : '', 'help' : 'List of members which are set dynamically and \ missed by pylint inference system, and so shouldn\'t trigger E0201 when \ accessed. Python regular expressions are accepted.'} - ), - ) + ), + ) def open(self): # do this in open since config not fully initialized in __init__ @@ -326,7 +325,7 @@ accessed. Python regular expressions are accepted.'} expr = node.func.expr klass = safe_infer(expr) if (klass is None or klass is astroid.YES or - not isinstance(klass, astroid.Instance)): + not isinstance(klass, astroid.Instance)): return try: @@ -354,12 +353,12 @@ accessed. Python regular expressions are accepted.'} property_like = False if isinstance(infered, astroid.Class): if (infered.root().name == BUILTINS and - infered.name == 'property'): + infered.name == 'property'): property_like = True else: for ancestor in infered.ancestors(): if (ancestor.name == 'property' and - ancestor.root().name == BUILTINS): + ancestor.root().name == BUILTINS): property_like = True break if property_like: diff --git a/checkers/utils.py b/checkers/utils.py index 01989be..d88d620 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -67,11 +67,10 @@ def clobber_in_except(node): if is_builtin(name): return (True, (name, 'builtins')) else: - scope, stmts = node.lookup(name) - if (stmts and - not isinstance(stmts[0].ass_type(), - (astroid.Assign, astroid.AugAssign, - astroid.ExceptHandler))): + stmts = node.lookup(name)[1] + if (stmts and not isinstance(stmts[0].ass_type(), + (astroid.Assign, astroid.AugAssign, + astroid.ExceptHandler))): return (True, (name, 'outer scope (line %s)' % stmts[0].fromlineno)) return (False, None) @@ -155,10 +154,10 @@ def is_defined_before(var_node): elif isinstance(_node, astroid.With): for expr, vars in _node.items: if expr.parent_of(var_node): - break + break if (vars and - isinstance(vars, astroid.AssName) and - vars.name == varname): + isinstance(vars, astroid.AssName) and + vars.name == varname): return True elif isinstance(_node, (astroid.Lambda, astroid.Function)): if _node.args.is_argument(varname): @@ -205,9 +204,9 @@ def is_func_decorator(node): if isinstance(parent, astroid.Decorators): return True if (parent.is_statement or - isinstance(parent, astroid.Lambda) or - isinstance(parent, (scoped_nodes.ComprehensionScope, - scoped_nodes.ListComp))): + isinstance(parent, astroid.Lambda) or + isinstance(parent, (scoped_nodes.ComprehensionScope, + scoped_nodes.ListComp))): break parent = parent.parent return False @@ -269,7 +268,7 @@ PYMETHODS = set(('__new__', '__init__', '__del__', '__hash__', '__or__', '__ior__', '__ror__', '__xor__', '__ixor__', '__rxor__', # XXX To be continued - )) + )) def check_messages(*messages): """decorator to store messages that are handled by a checker method""" @@ -365,7 +364,7 @@ def is_attr_protected(attrname): details), False otherwise. """ return attrname[0] == '_' and not attrname == '_' and not ( - attrname.startswith('__') and attrname.endswith('__')) + attrname.startswith('__') and attrname.endswith('__')) def node_frame_class(node): """return klass node for a method node (or a staticmethod or a @@ -386,8 +385,8 @@ def is_super_call(expr): is super. Check before that you're in a method. """ return (isinstance(expr, astroid.CallFunc) and - isinstance(expr.func, astroid.Name) and - expr.func.name == 'super') + isinstance(expr.func, astroid.Name) and + expr.func.name == 'super') def is_attr_private(attrname): """Check that attribute name is private (at least two leading underscores, diff --git a/checkers/variables.py b/checkers/variables.py index 3b9bcda..fc4c99e 100644 --- a/checkers/variables.py +++ b/checkers/variables.py @@ -27,10 +27,11 @@ from logilab.common.modutils import file_from_modpath from pylint.interfaces import IAstroidChecker from pylint.utils import get_global_option from pylint.checkers import BaseChecker -from pylint.checkers.utils import (PYMETHODS, is_ancestor_name, is_builtin, - is_defined_before, is_error, is_func_default, is_func_decorator, - assign_parent, check_messages, is_inside_except, clobber_in_except, - get_all_elements) +from pylint.checkers.utils import ( + PYMETHODS, is_ancestor_name, is_builtin, + is_defined_before, is_error, is_func_default, is_func_decorator, + assign_parent, check_messages, is_inside_except, clobber_in_except, + get_all_elements) def in_for_else_branch(parent, stmt): @@ -102,7 +103,7 @@ def _detect_global_scope(node, frame, defframe): (astroid.Function, astroid.Arguments)): return False elif any(not isinstance(f, (astroid.Class, astroid.Module)) - for f in (frame, defframe)): + for f in (frame, defframe)): # Not interested in other frames, since they are already # not in a global scope. return False @@ -234,8 +235,7 @@ class VariablesChecker(BaseChecker): name = 'variables' msgs = MSGS priority = -1 - options = ( - ("init-import", + options = (("init-import", {'default': 0, 'type' : 'yn', 'metavar' : '', 'help' : 'Tells whether we should check for unused import in \ __init__ files.'}), @@ -249,8 +249,8 @@ variables (i.e. expectedly not used).'}), 'metavar' : '', 'help' : 'List of additional names supposed to be defined in \ builtins. Remember that you should avoid to define new builtins when possible.' - }), - ) + }), + ) def __init__(self, linter=None): BaseChecker.__init__(self, linter) self._to_consume = None @@ -306,7 +306,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' self.add_message('undefined-all-variable', args=elt_name, node=elt) - except SyntaxError, exc: + except SyntaxError: # don't yield an syntax-error warning, # because it will be later yielded # when the file will be checked @@ -545,7 +545,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' def _is_direct_lambda_call(): return (isinstance(node_scope.parent, astroid.CallFunc) and node_scope.parent.func is node_scope) - + node_scope = node.scope() if not isinstance(node_scope, (astroid.Lambda, astroid.Function)): return @@ -594,7 +594,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' _astmts = astmts[:1] for i, stmt in enumerate(astmts[1:]): if (astmts[i].statement().parent_of(stmt) - and not in_for_else_branch(astmts[i].statement(), stmt)): + and not in_for_else_branch(astmts[i].statement(), stmt)): continue _astmts.append(stmt) astmts = _astmts @@ -634,7 +634,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' # a decorator, then start from the parent frame of the function instead # of the function frame - and thus open an inner class scope if (is_func_default(node) or is_func_decorator(node) - or is_ancestor_name(frame, node)): + or is_ancestor_name(frame, node)): start_index = len(self._to_consume) - 2 else: start_index = len(self._to_consume) - 1 @@ -648,7 +648,7 @@ builtins. Remember that you should avoid to define new builtins when possible.' # names. The only exception is when the starting scope is a # comprehension and its direct outer scope is a class if scope_type == 'class' and i != start_index and not ( - base_scope_type == 'comprehension' and i == start_index-1): + base_scope_type == 'comprehension' and i == start_index-1): # XXX find a way to handle class scope in a smoother way continue # the name has already been consumed, only check it's not a loop @@ -690,9 +690,11 @@ builtins. Remember that you should avoid to define new builtins when possible.' and name in child.names for child in defframe.get_children()) if (maybee0601 - and stmt.fromlineno <= defstmt.fromlineno - and not is_defined_before(node) - and not are_exclusive(stmt, defstmt, ('NameError', 'Exception', 'BaseException'))): + and stmt.fromlineno <= defstmt.fromlineno + and not is_defined_before(node) + and not are_exclusive(stmt, defstmt, ('NameError', + 'Exception', + 'BaseException'))): if defstmt is stmt and isinstance(node, (astroid.DelName, astroid.AssName)): self.add_message('undefined-variable', args=name, node=node) @@ -872,9 +874,8 @@ class VariablesChecker3k(VariablesChecker): name = metaclass.root().name if name: - found = consumed.setdefault(name, - module_locals.get(name, module_imports.get(name)) - ) + found = consumed.setdefault( + name, module_locals.get(name, module_imports.get(name))) if found is None and not metaclass: name = None diff --git a/epylint.py b/epylint.py index 24e9e95..beae481 100755 --- a/epylint.py +++ b/epylint.py @@ -46,7 +46,7 @@ You may also use py_run to run pylint with desired options and get back (or not) its output. """ -import sys, os, re +import sys, os import os.path as osp from subprocess import Popen, PIPE diff --git a/gui.py b/gui.py index a882a2b..fcc84e0 100644 --- a/gui.py +++ b/gui.py @@ -171,10 +171,11 @@ class LintGui(object): rightscrollbar.pack(side=RIGHT, fill=Y) bottomscrollbar = Scrollbar(msg_frame, orient=HORIZONTAL) bottomscrollbar.pack(side=BOTTOM, fill=X) - self.lb_messages = Listbox(msg_frame, - yscrollcommand=rightscrollbar.set, - xscrollcommand=bottomscrollbar.set, - bg="white") + self.lb_messages = Listbox( + msg_frame, + yscrollcommand=rightscrollbar.set, + xscrollcommand=bottomscrollbar.set, + bg="white") self.lb_messages.bind("", self.show_sourcefile) self.lb_messages.pack(expand=True, fill=BOTH) rightscrollbar.config(command=self.lb_messages.yview) @@ -185,10 +186,11 @@ class LintGui(object): rightscrollbar2.pack(side=RIGHT, fill=Y) bottomscrollbar2 = Scrollbar(history_frame, orient=HORIZONTAL) bottomscrollbar2.pack(side=BOTTOM, fill=X) - self.showhistory = Listbox(history_frame, - yscrollcommand=rightscrollbar2.set, - xscrollcommand=bottomscrollbar2.set, - bg="white") + self.showhistory = Listbox( + history_frame, + yscrollcommand=rightscrollbar2.set, + xscrollcommand=bottomscrollbar2.set, + bg="white") self.showhistory.pack(expand=True, fill=BOTH) rightscrollbar2.config(command=self.showhistory.yview) bottomscrollbar2.config(command=self.showhistory.xview) @@ -217,10 +219,11 @@ class LintGui(object): rightscrollbar.pack(side=RIGHT, fill=Y) bottomscrollbar = Scrollbar(res_frame, orient=HORIZONTAL) bottomscrollbar.pack(side=BOTTOM, fill=X) - self.results = Listbox(res_frame, - yscrollcommand=rightscrollbar.set, - xscrollcommand=bottomscrollbar.set, - bg="white", font="Courier") + self.results = Listbox( + res_frame, + yscrollcommand=rightscrollbar.set, + xscrollcommand=bottomscrollbar.set, + bg="white", font="Courier") self.results.pack(expand=True, fill=BOTH, side=BOTTOM) rightscrollbar.config(command=self.results.yview) bottomscrollbar.config(command=self.results.xview) @@ -399,8 +402,10 @@ class LintGui(object): def file_open(self, package=False, _=None): """launch a file browser""" if not package: - filename = askopenfilename(parent=self.root, filetypes=[('pythonfiles', '*.py'), - ('allfiles', '*')], title='Select Module') + filename = askopenfilename(parent=self.root, + filetypes=[('pythonfiles', '*.py'), + ('allfiles', '*')], + title='Select Module') else: filename = askdirectory(title="Select A Folder", mustexist=1) diff --git a/lint.py b/lint.py index 4184985..5be37f3 100644 --- a/lint.py +++ b/lint.py @@ -27,9 +27,8 @@ """ # import this first to avoid builtin namespace pollution -from pylint.checkers import utils +from pylint.checkers import utils #pylint: disable=unused-import -import functools import sys import os import tokenize @@ -43,14 +42,14 @@ from logilab.common.textutils import splitstrip, unquote from logilab.common.ureports import Table, Text, Section from logilab.common.__pkginfo__ import version as common_version -from astroid import MANAGER, nodes, AstroidBuildingException +from astroid import MANAGER, AstroidBuildingException from astroid.__pkginfo__ import version as astroid_version from astroid.modutils import load_module_from_name, get_module_part from pylint.utils import ( MSG_TYPES, OPTION_RGX, PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, - MessagesStore, FileState, EmptyReport, WarningScope, + MessagesStore, FileState, EmptyReport, expand_modules, tokenize_module) from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker from pylint.checkers import (BaseTokenChecker, @@ -65,7 +64,7 @@ from pylint.__pkginfo__ import version def _get_python_path(filepath): dirname = os.path.dirname(os.path.realpath( - os.path.expanduser(filepath))) + os.path.expanduser(filepath))) while True: if not os.path.exists(os.path.join(dirname, "__init__.py")): return dirname @@ -94,7 +93,7 @@ MSGS = { 'F0010': ('error while code parsing: %s', 'parse-error', 'Used when an exception occured while building the Astroid ' - 'representation which could be handled by astroid.'), + 'representation which could be handled by astroid.'), 'I0001': ('Unable to run raw checkers on built-in module %s', 'raw-checker-failed', @@ -183,7 +182,7 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, {'type' : 'csv', 'metavar' : '[,...]', 'dest' : 'black_list', 'default' : ('CVS',), 'help' : 'Add files or directories to the blacklist. ' - 'They should be base names, not paths.'}), + 'They should be base names, not paths.'}), ('persistent', {'default': True, 'type' : 'yn', 'metavar' : '', 'level': 1, @@ -193,85 +192,86 @@ class PyLinter(OptionsManagerMixIn, MessagesHandlerMixIn, ReportsHandlerMixIn, {'type' : 'csv', 'metavar' : '', 'default' : (), 'level': 1, 'help' : 'List of plugins (as comma separated values of ' - 'python modules names) to load, usually to register ' - 'additional checkers.'}), + 'python modules names) to load, usually to register ' + 'additional checkers.'}), ('output-format', {'default': 'text', 'type': 'string', 'metavar' : '', 'short': 'f', 'group': 'Reports', 'help' : 'Set the output format. Available formats are text,' - ' parseable, colorized, msvs (visual studio) and html. You ' - 'can also give a reporter class, eg mypackage.mymodule.' - 'MyReporterClass.'}), + ' parseable, colorized, msvs (visual studio) and html. You ' + 'can also give a reporter class, eg mypackage.mymodule.' + 'MyReporterClass.'}), ('files-output', {'default': 0, 'type' : 'yn', 'metavar' : '', 'group': 'Reports', 'level': 1, 'help' : 'Put messages in a separate file for each module / ' - 'package specified on the command line instead of printing ' - 'them on stdout. Reports (if any) will be written in a file ' - 'name "pylint_global.[txt|html]".'}), + 'package specified on the command line instead of printing ' + 'them on stdout. Reports (if any) will be written in a file ' + 'name "pylint_global.[txt|html]".'}), ('reports', {'default': 1, 'type' : 'yn', 'metavar' : '', 'short': 'r', 'group': 'Reports', 'help' : 'Tells whether to display a full report or only the ' - 'messages'}), + 'messages'}), ('evaluation', {'type' : 'string', 'metavar' : '', 'group': 'Reports', 'level': 1, 'default': '10.0 - ((float(5 * error + warning + refactor + ' - 'convention) / statement) * 10)', - 'help' : 'Python expression which should return a note less \ -than 10 (10 is the highest note). You have access to the variables errors \ -warning, statement which respectively contain the number of errors / warnings\ - messages and the total number of statements analyzed. This is used by the \ - global evaluation report (RP0004).'}), + 'convention) / statement) * 10)', + 'help' : 'Python expression which should return a note less ' + 'than 10 (10 is the highest note). You have access ' + 'to the variables errors warning, statement which ' + 'respectively contain the number of errors / ' + 'warnings messages and the total number of ' + 'statements analyzed. This is used by the global ' + 'evaluation report (RP0004).'}), ('comment', {'default': 0, 'type' : 'yn', 'metavar' : '', 'group': 'Reports', 'level': 1, 'help' : 'Add a comment according to your evaluation note. ' - 'This is used by the global evaluation report (RP0004).'}), + 'This is used by the global evaluation report (RP0004).'}), ('enable', {'type' : 'csv', 'metavar': '', 'short': 'e', 'group': 'Messages control', 'help' : 'Enable the message, report, category or checker with the ' - 'given id(s). You can either give multiple identifier ' - 'separated by comma (,) or put this option multiple time. ' - 'See also the "--disable" option for examples. '}), + 'given id(s). You can either give multiple identifier ' + 'separated by comma (,) or put this option multiple time. ' + 'See also the "--disable" option for examples. '}), ('disable', {'type' : 'csv', 'metavar': '', 'short': 'd', 'group': 'Messages control', 'help' : 'Disable the message, report, category or checker ' - 'with the given id(s). You can either give multiple identifiers' - ' separated by comma (,) or put this option multiple times ' - '(only on the command line, not in the configuration file ' - 'where it should appear only once).' - 'You can also use "--disable=all" to disable everything first ' - 'and then reenable specific checks. For example, if you want ' - 'to run only the similarities checker, you can use ' - '"--disable=all --enable=similarities". ' - 'If you want to run only the classes checker, but have no ' - 'Warning level messages displayed, use' - '"--disable=all --enable=classes --disable=W"'}), + 'with the given id(s). You can either give multiple identifiers' + ' separated by comma (,) or put this option multiple times ' + '(only on the command line, not in the configuration file ' + 'where it should appear only once).' + 'You can also use "--disable=all" to disable everything first ' + 'and then reenable specific checks. For example, if you want ' + 'to run only the similarities checker, you can use ' + '"--disable=all --enable=similarities". ' + 'If you want to run only the classes checker, but have no ' + 'Warning level messages displayed, use' + '"--disable=all --enable=classes --disable=W"'}), ('msg-template', {'type' : 'string', 'metavar': '