diff options
author | cpopa <devnull@localhost> | 2014-07-22 13:29:02 +0200 |
---|---|---|
committer | cpopa <devnull@localhost> | 2014-07-22 13:29:02 +0200 |
commit | 5359ed4f09b8667907280b49c84831a1c1dfe8bf (patch) | |
tree | a5f44cd333582ed59302f881bde5ddb5cbbdd9fb | |
parent | 7f5869347f70a4af11587a8975e262e9c813386b (diff) | |
parent | d2dac2c5d2ba2f8a06fe43d815fd3ff63a089000 (diff) | |
download | pylint-5359ed4f09b8667907280b49c84831a1c1dfe8bf.tar.gz |
Merge with default.
49 files changed, 1429 insertions, 86 deletions
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 @@ -2,11 +2,50 @@ ChangeLog for Pylint ==================== -- + * Emit 'undefined-variable' for undefined names when using the Python 3 `metaclass=` argument. + * 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). + * Don't warn with 'bad-format-character' when encountering + the 'a' format on Python 3. + + * 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'. + + * 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. + + * Emit 'not-callable' when calling properties. Closes issue #268. + + * Fix a false positive with unbalanced iterable unpacking, + when encountering starred nodes. Closes issue #273. + + * 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. + + * 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. + + * 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/classes.py b/checkers/classes.py index f5e2783..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. ' @@ -272,28 +277,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""" @@ -461,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/checkers/exceptions.py b/checkers/exceptions.py index 84f92ea..c91c95d 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', @@ -237,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) @@ -268,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/checkers/misc.py b/checkers/misc.py index d1b7c21..b4738b0 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,16 @@ class EncodingChecker(BaseChecker): msgs = MSGS options = (('notes', - {'type' : 'csv', 'metavar' : '<comma separated values>', - 'default' : ('FIXME', 'XXX', 'TODO'), - 'help' : 'List of note tags to take in consideration, \ -separated by a comma.' - }), - ) + {'type': 'csv', 'metavar': '<comma separated values>', + '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) + 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: @@ -71,19 +71,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)(:*\s*.+)' % "|".join(self.config.notes)) else: notes = None if module.file_encoding: encoding = module.file_encoding else: encoding = 'ascii' + for lineno, line in enumerate(stream): - line = self._check_encoding(lineno+1, line, encoding) + 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/checkers/strings.py b/checkers/strings.py index 04cf1bc..4fe16dd 100644 --- a/checkers/strings.py +++ b/checkers/strings.py @@ -20,6 +20,7 @@ import sys import tokenize +import string import astroid @@ -28,7 +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) +_PY3K = sys.version_info[:2] >= (3, 0) +_PY27 = sys.version_info[:2] == (2, 7) MSGS = { 'E1300': ("Unsupported format character %r (%#02x) at index %d", @@ -71,12 +73,121 @@ MSGS = { "too-few-format-args", "Used when a format string that uses unnamed conversion \ specifiers is given too few arguments"), + + 'W1302': ("Invalid format string", + "bad-format-string", + "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 " + "doesn't receive one or more required keywords."), + '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."), + '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}')."), + '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."), + '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.") } 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 _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): + 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): + """ + 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. + """ + 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) + 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 + 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 + 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 + +def get_access_path(key, parts): + """ Given a list of format specifiers, returns + the final access path (e.g. a.b.c[0][1]). + """ + path = [] + for is_attribute, specifier in parts: + if is_attribute: + path.append(".{}".format(specifier)) + else: + path.append("[{!r}]".format(specifier)) + return str(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. @@ -182,15 +293,157 @@ 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('bad-str-strip-call', 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('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. """ + try: + strnode = func.bound.infer().next() + except astroid.InferenceError: + return + if not isinstance(strnode, astroid.Const): + return + if node.starargs or node.kwargs: + # TODO: Don't complicate the logic, skip these for now. + return + try: + positional, named = get_args(node) + except astroid.InferenceError: + return + try: + fields, num_args = parse_format_method_string(strnode.value) + except utils.IncompleteFormatString: + 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)} + if manual_fields and num_args: + self.add_message('format-combined-specification', + node=node) + return + + 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=(field, )) + for field in named: + if field not in named_fields: + self.add_message('unused-format-string-argument', + node=node, + 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_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_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 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) + except utils.NoSuchArgumentError: + continue + else: + if key not in named: + continue + argument = named[key] + if argument in (astroid.YES, None): + continue + try: + argument = argument.infer().next() + except astroid.InferenceError: + continue + if not specifiers or argument is astroid.YES: + # No need to check this key if it doesn't + # use attribute / item access + continue + + previous = argument + parsed = [] + for is_attribute, specifier in specifiers: + if previous is astroid.YES: + break + 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()): + # Don't warn if the object has a custom __getattr__ + 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 astroid.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 = previous.infer().next() + except astroid.InferenceError: + # can't check further if we can't infer it + break + class StringConstantChecker(BaseTokenChecker): @@ -285,10 +538,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/checkers/typecheck.py b/checkers/typecheck.py index 4cb406f..3bcb099 100644 --- a/checkers/typecheck.py +++ b/checkers/typecheck.py @@ -18,9 +18,11 @@ import re import shlex +import sys import astroid from astroid import InferenceError, NotFoundError, YES, Instance +from astroid.bases import BUILTINS from pylint.interfaces import IAstroidChecker from pylint.checkers import BaseChecker @@ -74,8 +76,20 @@ MSGS = { ('Used when a function call does not pass a mandatory' ' keyword-only argument.'), {'minversion': (3, 0)}), + 'E1126': ('Sequence index is not an int, slice, or instance with __index__', + 'invalid-sequence-index', + '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 \ + 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. @@ -98,7 +112,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] @@ -139,7 +153,7 @@ class should be ignored. A mixin class is detected if its name ends with \ 'metavar': '<module names>', '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', @@ -293,7 +307,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 +394,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 @@ -445,6 +527,121 @@ 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.parent or not hasattr(node.parent, "value"): + return + + # 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__. + + parent_type = safe_infer(node.parent.value) + + if not isinstance(parent_type, (astroid.Class, astroid.Instance)): + return + + # 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 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: + methods = parent_type.getattr(methodname) + if methods is astroid.YES: + return + itemmethod = methods[0] + except (astroid.NotFoundError, IndexError): + return + + if not isinstance(itemmethod, astroid.Function): + return + + if itemmethod.root().name != BUILTINS: + return + + if not itemmethod.parent: + return + + if itemmethod.parent.name not in sequence_types: + return + + # 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 + + # Constants must be of type int + if isinstance(index_type, astroid.Const): + if isinstance(index_type.value, int): + return + # Instance values must be int, slice, or have an __index__ method + elif isinstance(index_type, astroid.Instance): + if index_type.pytype() in (BUILTINS + '.int', BUILTINS + '.slice'): + return + + try: + index_type.getattr('__index__') + return + except astroid.NotFoundError: + pass + + # Anything else is an error + self.add_message('invalid-sequence-index', node=node) + + @check_messages('invalid-slice-index') + def visit_slice(self, node): + # 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 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, 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.pytype() in (BUILTINS + '.int', + BUILTINS + '.NoneType'): + continue + + try: + index_type.getattr('__index__') + return + except astroid.NotFoundError: + pass + + # Anything else is an error + self.add_message('invalid-slice-index', node=node) def register(linter): """required method to auto register this checker """ diff --git a/checkers/utils.py b/checkers/utils.py index e7d85d4..01989be 100644 --- a/checkers/utils.py +++ b/checkers/utils.py @@ -19,6 +19,7 @@ """ import re +import sys import string import astroid @@ -26,8 +27,8 @@ 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 +PY3K = sys.version_info[0] == 3 class NoSuchArgumentError(Exception): @@ -345,7 +346,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) @@ -354,6 +359,7 @@ def parse_format_string(format_string): i += 1 return keys, num_args + def is_attr_protected(attrname): """return True if attribute name is protected (start with _ and some other details), False otherwise. diff --git a/checkers/variables.py b/checkers/variables.py index ebe520a..6fba97d 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, @@ -68,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', @@ -147,7 +213,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.'), @@ -355,6 +421,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): @@ -364,6 +434,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: @@ -411,7 +498,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 @@ -447,7 +553,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 @@ -552,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 @@ -649,6 +755,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), @@ -675,6 +785,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__': @@ -685,7 +797,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/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 @@ -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,7 +570,10 @@ 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 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 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() diff --git a/test/input/func_assigning_non_slot.py b/test/input/func_assigning_non_slot.py new file mode 100644 index 0000000..9c91dd2 --- /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(object): + """ 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/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 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_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/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/input/func_fixme.py b/test/input/func_fixme.py index b6371f1..66e86db 100644 --- a/test/input/func_fixme.py +++ b/test/input/func_fixme.py @@ -1,9 +1,14 @@ """docstring""" - +# pylint: disable=W0612 __revision__ = '' # FIXME: beep + 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/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/input/func_invalid_sequence_index.py b/test/input/func_invalid_sequence_index.py new file mode 100644 index 0000000..b60e0b5 --- /dev/null +++ b/test/input/func_invalid_sequence_index.py @@ -0,0 +1,210 @@ +"""Errors for invalid sequence indices""" +# pylint: disable=too-few-public-methods, no-self-use + +__revision__ = 0 + +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 None""" + return TESTLIST[None] + +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 function10(): + """list index is a integer expression""" + return TESTLIST[int(0.0)] # no error + +def function11(): + """list index is a slice""" + return TESTLIST[slice(1, 2, 3)] # no error + +def function12(): + """list index implements __index__""" + class IndexType(object): + """Class with __index__ method""" + def __index__(self): + """Allow objects of this class to be used as slice indices""" + return 0 + + return TESTLIST[IndexType()] # no error + +def function13(): + """list index implements __index__ in a superclass""" + class IndexType(object): + """Class with __index__ method""" + def __index__(self): + """Allow objects of this class to be used as slice indices""" + return 0 + + class IndexSubType(IndexType): + """Class with __index__ in parent""" + pass + + return TESTLIST[IndexSubType()] # no error + +def function14(): + """Tuple index is an int constant""" + return TESTTUPLE[0] + +def function15(): + """String index is an int constant""" + return TESTSTR[0] + +def function16(): + """Index of subclass of tuple is an int constant""" + 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): + """Subclass of tuple with custom __getitem__""" + 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): + """Subclass of tuple with custom __getitem__""" + def __getitem__(self, index): + """Allow non-integer indices""" + return 0 + + class SubTupleTest(TupleTest): + """Subclass of a subclass of tuple""" + 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() + + # Set and delete with invalid indices + test[None] = 0 + del test[None] + + # Set and delete with valid indices + test[0] = 0 # no error + 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 # failure on the getitem with None + del test[None] + + 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__""" + class ListTest(list): + """Override delitem but not get or set""" + def __delitem__(self, key): + pass + test = ListTest() + + 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__""" + class ListTest(list): + """Override gelitem but not del or set""" + def __getitem__(self, key): + pass + test = ListTest() + + 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] + +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/input/func_invalid_slice_index.py b/test/input/func_invalid_slice_index.py new file mode 100644 index 0000000..32f2f2d --- /dev/null +++ b/test/input/func_invalid_slice_index.py @@ -0,0 +1,61 @@ +"""Errors for invalid slice indices""" +# pylint: disable=too-few-public-methods, no-self-use + +__revision__ = 0 + +TESTLIST = [1, 2, 3] + +# Invalid indices +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): + """Class with no __index__ method""" + pass + + 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] # no error + +def function6(): + """class with __index__ used as index""" + class IndexTest(object): + """Class with __index__ method""" + def __index__(self): + """Allow objects of this class to be used as slice indices""" + return 0 + + return TESTLIST[IndexTest():None:None] # no error + +def function7(): + """class with __index__ in superclass used as index""" + class IndexType(object): + """Class with __index__ method""" + def __index__(self): + """Allow objects of this class to be used as slice indices""" + return 0 + + class IndexSubType(IndexType): + """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)] # no error 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) diff --git a/test/input/func_string_format_py27.py b/test/input/func_string_format_py27.py new file mode 100644 index 0000000..d2f5599 --- /dev/null +++ b/test/input/func_string_format_py27.py @@ -0,0 +1,78 @@ +"""test for Python 3 string formatting error
+"""
+# pylint: disable=too-few-public-methods, import-error, unused-argument, star-args
+import os
+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 log(message, message_type="error"):
+ """ Test """
+ return message
+
+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 "{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)
+ print "{0} {1} {a}".format(1, 2, 3)
+ print "{a.ids.__len__.length}".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][0]}".format(((1, )))
+ print "{b[0]}".format(a=23)
+ print "{a[0]}".format(a=object)
+ print log("{}".format(2, "info"))
+ print "{0.missing}".format(2)
diff --git a/test/input/func_typecheck_non_callable_call.py b/test/input/func_typecheck_non_callable_call.py index 8d8a6c2..6d56b90 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(object): + """ 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/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/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/input/func_w0623_py_30.py b/test/input/func_w0623_py_30.py index 9bccbc6..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,pointless-except __revision__ = '' import exceptions 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 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__ 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 diff --git a/test/messages/func_fixme.txt b/test/messages/func_fixme.txt index 2544ce8..88199a7 100644 --- a/test/messages/func_fixme.txt +++ b/test/messages/func_fixme.txt @@ -1,2 +1,4 @@ W: 5: FIXME: beep -W: 8: XXX:bop''' +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 diff --git a/test/messages/func_invalid_sequence_index.txt b/test/messages/func_invalid_sequence_index.txt new file mode 100644 index 0000000..db9edab --- /dev/null +++ b/test/messages/func_invalid_sequence_index.txt @@ -0,0 +1,19 @@ +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:144:function21: 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 new file mode 100644 index 0000000..d5b9e86 --- /dev/null +++ b/test/messages/func_invalid_slice_index.txt @@ -0,0 +1,5 @@ +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__ diff --git a/test/messages/func_string_format_py27.txt b/test/messages/func_string_format_py27.txt new file mode 100644 index 0000000..56f8c41 --- /dev/null +++ b/test/messages/func_string_format_py27.txt @@ -0,0 +1,26 @@ +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 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 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 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' 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 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.""" @@ -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 |