summaryrefslogtreecommitdiff
path: root/checkers
diff options
context:
space:
mode:
authorcpopa <devnull@localhost>2014-01-09 14:50:10 +0200
committercpopa <devnull@localhost>2014-01-09 14:50:10 +0200
commit79ded0d5f158f19569b53f02e53f9eb8b5be1e3d (patch)
tree5f3a3e058493641d6e1edf9df334acef64209086 /checkers
parent95ec900f33bae5dcf71a00cdf4ca4ab7392d604f (diff)
parent9e0dfe50de779a4fdf4d2abe80587f541c7cb71c (diff)
downloadpylint-79ded0d5f158f19569b53f02e53f9eb8b5be1e3d.tar.gz
Merge with default.
Diffstat (limited to 'checkers')
-rw-r--r--checkers/__init__.py4
-rw-r--r--checkers/base.py22
-rw-r--r--checkers/classes.py8
-rw-r--r--checkers/design_analysis.py49
-rw-r--r--checkers/exceptions.py42
-rw-r--r--checkers/format.py418
-rw-r--r--checkers/imports.py11
-rw-r--r--checkers/logging.py34
-rw-r--r--checkers/newstyle.py5
-rw-r--r--checkers/raw_metrics.py4
-rw-r--r--checkers/similar.py18
-rw-r--r--checkers/stdlib.py2
-rw-r--r--checkers/strings.py6
-rw-r--r--checkers/typecheck.py4
-rw-r--r--checkers/utils.py68
-rw-r--r--checkers/variables.py64
16 files changed, 486 insertions, 273 deletions
diff --git a/checkers/__init__.py b/checkers/__init__.py
index 27dc364..1d0aa42 100644
--- a/checkers/__init__.py
+++ b/checkers/__init__.py
@@ -99,10 +99,6 @@ class BaseChecker(OptionsProviderMixIn, ASTWalker):
"""add a message of a given type"""
self.linter.add_message(msg_id, line, node, args)
- def package_dir(self):
- """return the base directory for the analysed package"""
- return dirname(self.linter.base_file)
-
# dummy methods implementing the IChecker interface
def open(self):
diff --git a/checkers/base.py b/checkers/base.py
index 6aae709..5df0477 100644
--- a/checkers/base.py
+++ b/checkers/base.py
@@ -52,6 +52,10 @@ NO_REQUIRED_DOC_RGX = re.compile('__.*__')
REVERSED_METHODS = (('__getitem__', '__len__'),
('__reversed__', ))
+BAD_FUNCTIONS = ['map', 'filter', 'apply']
+if sys.version_info < (3, 0):
+ BAD_FUNCTIONS.append('input')
+
del re
def in_loop(node):
@@ -83,12 +87,19 @@ def _loop_exits_early(loop):
# in orelse.
for child in loop.body:
if isinstance(child, loop_nodes):
+ # break statement may be in orelse of child loop.
+ for orelse in (child.orelse or ()):
+ for _ in orelse.nodes_of_class(astroid.Break, skip_klass=loop_nodes):
+ return True
continue
for _ in child.nodes_of_class(astroid.Break, skip_klass=loop_nodes):
return True
return False
-
+if sys.version_info < (3, 0):
+ PROPERTY_CLASSES = set(('__builtin__.property', 'abc.abstractproperty'))
+else:
+ PROPERTY_CLASSES = set(('builtins.property', 'abc.abstractproperty'))
def _determine_function_name_type(node):
"""Determine the name type whose regex the a function's name should match.
@@ -109,8 +120,7 @@ def _determine_function_name_type(node):
(isinstance(decorator, astroid.Getattr) and
decorator.attrname == 'abstractproperty')):
infered = safe_infer(decorator)
- if (infered and
- infered.qname() in ('__builtin__.property', 'abc.abstractproperty')):
+ if infered and infered.qname() in PROPERTY_CLASSES:
return 'attr'
# If the function is decorated using the prop_method.{setter,getter}
# form, treat it like an attribute as well.
@@ -251,7 +261,7 @@ class BasicErrorChecker(_BasicChecker):
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
@@ -432,13 +442,13 @@ functions, methods
'comma'}
),
('bad-functions',
- {'default' : ('map', 'filter', 'apply', 'input'),
+ {'default' : BAD_FUNCTIONS,
'type' :'csv', 'metavar' : '<builtin function names>',
'help' : 'List of builtins function names that should not be '
'used, separated by a comma'}
),
)
- reports = ( ('RP0101', 'Statistics by type', report_by_type_stats), )
+ reports = (('RP0101', 'Statistics by type', report_by_type_stats),)
def __init__(self, linter):
_BasicChecker.__init__(self, linter)
diff --git a/checkers/classes.py b/checkers/classes.py
index fd76146..fc09021 100644
--- a/checkers/classes.py
+++ b/checkers/classes.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2003-2012 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
@@ -183,7 +183,7 @@ class ClassChecker(BaseChecker):
options = (('ignore-iface-methods',
{'default' : (#zope interface
'isImplementedBy', 'deferred', 'extends', 'names',
- 'namesAndDescriptions', 'queryDescriptionFor', 'getBases',
+ 'namesAndDescriptions', 'queryDescriptionFor', 'getBases',
'getDescriptionFor', 'getDoc', 'getName', 'getTaggedValue',
'getTaggedValueTags', 'isEqualOrExtendedBy', 'setTaggedValue',
'isImplementedByInstancesOf',
@@ -355,10 +355,10 @@ a metaclass class method.'}
positional = sum(1 for arg in node.args.args if arg.name != 'self')
if positional < 3 and not node.args.vararg:
self.add_message('bad-context-manager',
- node=node)
+ node=node)
elif positional > 3:
self.add_message('bad-context-manager',
- node=node)
+ node=node)
def leave_function(self, node):
"""on method node, check if this method couldn't be a function
diff --git a/checkers/design_analysis.py b/checkers/design_analysis.py
index f3b5882..11defbf 100644
--- a/checkers/design_analysis.py
+++ b/checkers/design_analysis.py
@@ -26,42 +26,6 @@ import re
# regexp for ignored argument name
IGNORED_ARGUMENT_NAMES = re.compile('_.*')
-SPECIAL_METHODS = [('Context manager', set(('__enter__',
- '__exit__',))),
- ('Container', set(('__len__',
- '__getitem__',))),
- ('Mutable container', set(('__setitem__',
- '__delitem__',))),
- ]
-
-class SpecialMethodChecker(object):
- """A functor that checks for consistency of a set of special methods"""
- def __init__(self, methods_found, on_error):
- """Stores the set of __x__ method names that were found in the
- class and a callable that will be called with args to R0024 if
- the check fails
- """
- self.methods_found = methods_found
- self.on_error = on_error
-
- def __call__(self, methods_required, protocol):
- """Checks the set of method names given to __init__ against the set
- required.
-
- If they are all present, returns true.
- If they are all absent, returns false.
- If some are present, reports the error and returns false.
- """
- required_methods_found = methods_required & self.methods_found
- if required_methods_found == methods_required:
- return True
- if required_methods_found:
- required_methods_missing = methods_required - self.methods_found
- self.on_error((protocol,
- ', '.join(sorted(required_methods_found)),
- ', '.join(sorted(required_methods_missing))))
- return False
-
def class_is_abstract(klass):
"""return true if the given class node should be considered as an abstract
@@ -121,10 +85,6 @@ MSGS = {
'R0923': ('Interface not implemented',
'interface-not-implemented',
'Used when an interface class is not implemented anywhere.'),
- 'R0924': ('Badly implemented %s, implements %s but not %s',
- 'incomplete-protocol',
- 'A class implements some of the special methods for a particular \
- protocol, but not all of them')
}
@@ -289,13 +249,6 @@ class MisdesignChecker(BaseChecker):
# stop here for exception, metaclass and interface classes
if node.type != 'class':
return
- # Does the class implement special methods consitently?
- # If so, don't enforce minimum public methods.
- check_special = SpecialMethodChecker(
- special_methods, lambda args: self.add_message('R0924', node=node, args=args))
- protocols = [check_special(pmethods, pname) for pname, pmethods in SPECIAL_METHODS]
- if True in protocols:
- return
# Does the class contain more than 5 public methods ?
if nb_public_methods < self.config.min_public_methods:
self.add_message('R0903', node=node,
@@ -379,7 +332,7 @@ class MisdesignChecker(BaseChecker):
"""increments the branches counter"""
branches = 1
# don't double count If nodes coming from some 'elif'
- if node.orelse and (len(node.orelse)>1 or
+ if node.orelse and (len(node.orelse) > 1 or
not isinstance(node.orelse[0], If)):
branches += 1
self._inc_branch(branches)
diff --git a/checkers/exceptions.py b/checkers/exceptions.py
index 8ac00a5..5bb07ac 100644
--- a/checkers/exceptions.py
+++ b/checkers/exceptions.py
@@ -25,6 +25,23 @@ from pylint.checkers import BaseChecker
from pylint.checkers.utils import is_empty, is_raising, check_messages
from pylint.interfaces import IAstroidChecker
+def infer_bases(klass):
+ """ Fully infer the bases of the klass node.
+
+ This doesn't use .ancestors(), because we need
+ the non-inferable nodes (YES nodes),
+ which can't be retrieved from .ancestors()
+ """
+ for base in klass.bases:
+ try:
+ inferit = base.infer().next()
+ except astroid.InferenceError:
+ continue
+ if inferit is YES:
+ yield inferit
+ else:
+ for base in infer_bases(inferit):
+ yield base
OVERGENERAL_EXCEPTIONS = ('Exception',)
@@ -50,7 +67,7 @@ MSGS = {
'catching-non-exception',
'Used when a class which doesn\'t inherit from \
BaseException is used as an exception in an except clause.'),
-
+
'W0701': ('Raising a string exception',
'raising-string',
'Used when a string exception is raised.'),
@@ -141,10 +158,10 @@ class ExceptionsChecker(BaseChecker):
isinstance(expr, (astroid.List, astroid.Dict, astroid.Tuple,
astroid.Module, astroid.Function)):
self.add_message('E0702', node=node, args=expr.name)
- elif ( (isinstance(expr, astroid.Name) and expr.name == 'NotImplemented')
- or (isinstance(expr, astroid.CallFunc) and
- isinstance(expr.func, astroid.Name) and
- expr.func.name == 'NotImplemented') ):
+ elif ((isinstance(expr, astroid.Name) and expr.name == 'NotImplemented')
+ or (isinstance(expr, astroid.CallFunc) and
+ isinstance(expr.func, astroid.Name) and
+ expr.func.name == 'NotImplemented')):
self.add_message('E0711', node=node)
elif isinstance(expr, astroid.BinOp) and expr.op == '%':
self.add_message('W0701', node=node)
@@ -211,12 +228,19 @@ class ExceptionsChecker(BaseChecker):
and exc.root().name == EXCEPTIONS_MODULE
and nb_handlers == 1 and not is_raising(handler.body)):
self.add_message('W0703', args=exc.name, node=handler.type)
-
+
if (not inherit_from_std_ex(exc) and
exc.root().name != BUILTINS_NAME):
- self.add_message('catching-non-exception',
- node=handler.type,
- args=(exc.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
+ bases = infer_bases(exc)
+ fully_infered = all(inferit is not YES
+ for inferit in bases)
+ if fully_infered:
+ self.add_message('catching-non-exception',
+ node=handler.type,
+ args=(exc.name, ))
exceptions_classes += excs
diff --git a/checkers/format.py b/checkers/format.py
index 1cf0edc..aab2320 100644
--- a/checkers/format.py
+++ b/checkers/format.py
@@ -21,19 +21,43 @@ http://www.python.org/doc/essays/styleguide.html
Some parts of the process_token method is based from The Tab Nanny std module.
"""
-import re, sys
+import keyword
+import sys
import tokenize
+
if not hasattr(tokenize, 'NL'):
raise ValueError("tokenize.NL doesn't exist -- tokenize module too old")
-from logilab.common.textutils import pretty_match
from astroid import nodes
-from pylint.interfaces import ITokenChecker, IAstroidChecker
+from pylint.interfaces import ITokenChecker, IAstroidChecker, IRawChecker
from pylint.checkers import BaseTokenChecker
from pylint.checkers.utils import check_messages
from pylint.utils import WarningScope, OPTION_RGX
+_KEYWORD_TOKENS = ['assert', 'del', 'elif', 'except', 'for', 'if', 'in', 'not',
+ 'raise', 'return', 'while', 'yield']
+if sys.version_info < (3, 0):
+ _KEYWORD_TOKENS.append('print')
+
+_SPACED_OPERATORS = ['==', '<', '>', '!=', '<>', '<=', '>=',
+ '+=', '-=', '*=', '**=', '/=', '//=', '&=', '|=', '^=',
+ '%=', '>>=', '<<=']
+_OPENING_BRACKETS = ['(', '[', '{']
+_CLOSING_BRACKETS = [')', ']', '}']
+
+_EOL = frozenset([tokenize.NEWLINE, tokenize.NL, tokenize.COMMENT])
+
+# Whitespace checking policy constants
+_MUST = 0
+_MUST_NOT = 1
+_IGNORE = 2
+
+# Whitespace checking config constants
+_DICT_SEPARATOR = 'dict-separator'
+_TRAILING_COMMA = 'trailing-comma'
+_NO_SPACE_CHECK_CHOICES = [_TRAILING_COMMA, _DICT_SEPARATOR]
+
MSGS = {
'C0301': ('Line too long (%s/%s)',
'line-too-long',
@@ -64,22 +88,20 @@ MSGS = {
'multiple-statements',
'Used when more than on statement are found on the same line.',
{'scope': WarningScope.NODE}),
- 'C0322': ('Operator not preceded by a space\n%s',
- 'no-space-before-operator',
- 'Used when one of the following operator (!= | <= | == | >= | < '
- '| > | = | \\+= | -= | \\*= | /= | %) is not preceded by a space.',
- {'scope': WarningScope.NODE}),
- 'C0323': ('Operator not followed by a space\n%s',
- 'no-space-after-operator',
- 'Used when one of the following operator (!= | <= | == | >= | < '
- '| > | = | \\+= | -= | \\*= | /= | %) is not followed by a space.',
- {'scope': WarningScope.NODE}),
- 'C0324': ('Comma not followed by a space\n%s',
- 'no-space-after-comma',
- 'Used when a comma (",") is not followed by a space.',
- {'scope': WarningScope.NODE}),
+ 'C0325' : ('Unnecessary parens after %r keyword',
+ 'superfluous-parens',
+ 'Used when a single item in parentheses follows an if, for, or '
+ 'other keyword.'),
+ 'C0326': ('%s space %s %s %s\n%s',
+ 'bad-whitespace',
+ ('Used when a wrong number of spaces is used around an operator, '
+ 'bracket or block opener.'),
+ {'old_names': [('C0323', 'no-space-after-operator'),
+ ('C0324', 'no-space-after-comma'),
+ ('C0322', 'no-space-before-operator')]})
}
+
if sys.version_info < (3, 0):
MSGS.update({
@@ -99,74 +121,21 @@ if sys.version_info < (3, 0):
{'scope': WarningScope.NODE}),
})
-# simple quoted string rgx
-SQSTRING_RGX = r'"([^"\\]|\\.)*?"'
-# simple apostrophed rgx
-SASTRING_RGX = r"'([^'\\]|\\.)*?'"
-# triple quoted string rgx
-TQSTRING_RGX = r'"""([^"]|("(?!"")))*?(""")'
-# triple apostrophe'd string rgx
-TASTRING_RGX = r"'''([^']|('(?!'')))*?(''')"
-
-# finally, the string regular expression
-STRING_RGX = re.compile('(%s)|(%s)|(%s)|(%s)' % (TQSTRING_RGX, TASTRING_RGX,
- SQSTRING_RGX, SASTRING_RGX),
- re.MULTILINE|re.DOTALL)
-
-COMMENT_RGX = re.compile("#.*$", re.M)
-
-OPERATORS = r'!=|<=|==|>=|<|>|=|\+=|-=|\*=|/=|%'
-
-OP_RGX_MATCH_1 = r'[^(]*(?<!\s|\^|<|>|=|\+|-|\*|/|!|%%|&|\|)(%s).*' % OPERATORS
-OP_RGX_SEARCH_1 = r'(?<!\s|\^|<|>|=|\+|-|\*|/|!|%%|&|\|)(%s)' % OPERATORS
-OP_RGX_MATCH_2 = r'[^(]*(%s)(?!\s|=|>|<).*' % OPERATORS
-OP_RGX_SEARCH_2 = r'(%s)(?!\s|=|>)' % OPERATORS
+def _underline_token(token):
+ length = token[3][1] - token[2][1]
+ offset = token[2][1]
+ return token[4] + (' ' * offset) + ('^' * length)
-BAD_CONSTRUCT_RGXS = (
- (re.compile(OP_RGX_MATCH_1, re.M),
- re.compile(OP_RGX_SEARCH_1, re.M),
- 'C0322'),
-
- (re.compile(OP_RGX_MATCH_2, re.M),
- re.compile(OP_RGX_SEARCH_2, re.M),
- 'C0323'),
-
- (re.compile(r'.*,[^(\s|\]|}|\))].*', re.M),
- re.compile(r',[^\s)]', re.M),
- 'C0324'),
- )
-
-
-def get_string_coords(line):
- """return a list of string positions (tuple (start, end)) in the line
- """
- result = []
- for match in re.finditer(STRING_RGX, line):
- result.append( (match.start(), match.end()) )
- return result
-
-def in_coords(match, string_coords):
- """return true if the match is in the string coord"""
- mstart = match.start()
- for start, end in string_coords:
- if mstart >= start and mstart < end:
- return True
- return False
-
-def check_line(line):
- """check a line for a bad construction
- if it founds one, return a message describing the problem
- else return None
- """
- cleanstr = COMMENT_RGX.sub('', STRING_RGX.sub('', line))
- for rgx_match, rgx_search, msg_id in BAD_CONSTRUCT_RGXS:
- if rgx_match.match(cleanstr):
- string_positions = get_string_coords(line)
- for match in re.finditer(rgx_search, line):
- if not in_coords(match, string_positions):
- return msg_id, pretty_match(match, line.rstrip())
+def _column_distance(token1, token2):
+ if token1 == token2:
+ return 0
+ if token2[3] < token1[3]:
+ token1, token2 = token2, token1
+ if token1[3][0] != token2[2][0]:
+ return None
+ return token2[2][1] - token1[3][1]
class FormatChecker(BaseTokenChecker):
@@ -177,7 +146,7 @@ class FormatChecker(BaseTokenChecker):
* use of <> instead of !=
"""
- __implements__ = (ITokenChecker, IAstroidChecker)
+ __implements__ = (ITokenChecker, IAstroidChecker, IRawChecker)
# configuration section name
name = 'format'
@@ -193,6 +162,16 @@ class FormatChecker(BaseTokenChecker):
'default': r'^\s*(# )?<?https?://\S+>?$',
'help': ('Regexp for a line that is allowed to be longer than '
'the limit.')}),
+ ('single-line-if-stmt',
+ {'default': False, 'type' : 'yn', 'metavar' : '<y_or_n>',
+ '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',
+ 'choices': _NO_SPACE_CHECK_CHOICES,
+ 'help': ('List of optional constructs for which whitespace '
+ 'checking is disabled')}),
('max-module-lines',
{'default' : 1000, 'type' : 'int', 'metavar' : '<int>',
'help': 'Maximum number of lines in a module'}
@@ -213,6 +192,223 @@ class FormatChecker(BaseTokenChecker):
self._lines[line_num] = line.split('\n')[0]
self.check_lines(line, line_num)
+ 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')
+
+ def _check_keyword_parentheses(self, tokens, start):
+ """Check that there are not unnecessary parens after a keyword.
+
+ Parens are unnecessary if there is exactly one balanced outer pair on a
+ line, and it is followed by a colon, and contains no commas (i.e. is not a
+ tuple).
+
+ Args:
+ tokens: list of Tokens; the entire list of Tokens.
+ start: int; the position of the keyword in the token list.
+ """
+ # If the next token is not a paren, we're fine.
+ if tokens[start+1][1] != '(':
+ return
+
+ found_and_or = False
+ depth = 0
+ keyword_token = tokens[start][1]
+ line_num = tokens[start][2][0]
+
+ for i in xrange(start, len(tokens) - 1):
+ token = tokens[i]
+
+ # If we hit a newline, then assume any parens were for continuation.
+ if token[0] == tokenize.NL:
+ return
+
+ if token[1] == '(':
+ depth += 1
+ elif token[1] == ')':
+ depth -= 1
+ 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)):
+ # The empty tuple () is always accepted.
+ if i == start + 2:
+ return
+ if keyword_token == 'not':
+ if not found_and_or:
+ self.add_message('C0325', line=line_num,
+ args=keyword_token)
+ elif keyword_token in ('return', 'yield'):
+ self.add_message('C0325', 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,
+ args=keyword_token)
+ return
+ elif depth == 1:
+ # This is a tuple, which is always acceptable.
+ if token[1] == ',':
+ return
+ # 'and' and 'or' are the only boolean operators with lower precedence
+ # than 'not', so parens are only required when they are found.
+ elif token[1] in ('and', 'or'):
+ found_and_or = True
+ # A yield inside an expression must always be in parentheses,
+ # quit early without error.
+ elif token[1] == 'yield':
+ return
+ # A generator expression always has a 'for' token in it, and
+ # the 'for' token is only legal inside parens when it is in a
+ # generator expression. The parens are necessary here, so bail
+ # without an error.
+ elif token[1] == 'for':
+ return
+
+ def _opening_bracket(self, tokens, i):
+ self._bracket_stack.append(tokens[i][1])
+ # Special case: ignore slices
+ if tokens[i][1] == '[' and tokens[i+1][1] == ':':
+ return
+
+ if (i > 0 and (tokens[i-1][0] == tokenize.NAME and
+ not (keyword.iskeyword(tokens[i-1][1]))
+ or tokens[i-1][1] in _CLOSING_BRACKETS)):
+ self._check_space(tokens, i, (_MUST_NOT, _MUST_NOT))
+ else:
+ self._check_space(tokens, i, (_IGNORE, _MUST_NOT))
+
+ def _closing_bracket(self, tokens, i):
+ self._bracket_stack.pop()
+ # Special case: ignore slices
+ if tokens[i-1][1] == ':' and tokens[i][1] == ']':
+ return
+ policy_before = _MUST_NOT
+ if tokens[i][1] in _CLOSING_BRACKETS and tokens[i-1][1] == ',':
+ if _TRAILING_COMMA in self.config.no_space_check:
+ policy_before = _IGNORE
+
+ self._check_space(tokens, i, (policy_before, _IGNORE))
+
+ def _check_equals_spacing(self, tokens, i):
+ """Check the spacing of a single equals sign."""
+ if self._inside_brackets('(') or self._inside_brackets('lambda'):
+ self._check_space(tokens, i, (_MUST_NOT, _MUST_NOT))
+ else:
+ self._check_space(tokens, i, (_MUST, _MUST))
+
+ def _open_lambda(self, tokens, i): # pylint:disable=unused-argument
+ self._bracket_stack.append('lambda')
+
+ def _handle_colon(self, tokens, i):
+ # Special case: ignore slices
+ if self._inside_brackets('['):
+ return
+ if (self._inside_brackets('{') and
+ _DICT_SEPARATOR in self.config.no_space_check):
+ policy = (_IGNORE, _IGNORE)
+ else:
+ policy = (_MUST_NOT, _MUST)
+ self._check_space(tokens, i, policy)
+
+ if self._inside_brackets('lambda'):
+ self._bracket_stack.pop()
+
+ def _handle_comma(self, tokens, i):
+ # Only require a following whitespace if this is
+ # not a hanging comma before a closing bracket.
+ if tokens[i+1][1] in _CLOSING_BRACKETS:
+ self._check_space(tokens, i, (_MUST_NOT, _IGNORE))
+ else:
+ self._check_space(tokens, i, (_MUST_NOT, _MUST))
+
+ def _check_surrounded_by_space(self, tokens, i):
+ """Check that a binary operator is surrounded by exactly one space."""
+ self._check_space(tokens, i, (_MUST, _MUST))
+
+ def _check_space(self, tokens, i, policies):
+ def _policy_string(policy):
+ if policy == _MUST:
+ return 'Exactly one', 'required'
+ else:
+ return 'No', 'allowed'
+
+ def _name_construct(token):
+ if tokens[i][1] == ',':
+ return 'comma'
+ elif tokens[i][1] == ':':
+ return ':'
+ elif tokens[i][1] in '()[]{}':
+ return 'bracket'
+ elif tokens[i][1] in ('<', '>', '<=', '>=', '!='):
+ return 'comparison'
+ else:
+ if self._inside_brackets('('):
+ return 'keyword argument assignment'
+ else:
+ return 'assignment'
+
+ good_space = [True, True]
+ pairs = [(tokens[i-1], tokens[i]), (tokens[i], tokens[i+1])]
+
+ for other_idx, (policy, token_pair) in enumerate(zip(policies, pairs)):
+ if token_pair[other_idx][0] in _EOL or policy == _IGNORE:
+ continue
+
+ distance = _column_distance(*token_pair)
+ if distance is None:
+ continue
+ good_space[other_idx] = (
+ (policy == _MUST and distance == 1) or
+ (policy == _MUST_NOT and distance == 0))
+
+ warnings = []
+ if not any(good_space) and policies[0] == policies[1]:
+ warnings.append((policies[0], 'around'))
+ else:
+ for ok, policy, position in zip(good_space, policies, ('before', 'after')):
+ if not ok:
+ warnings.append((policy, position))
+ for policy, position in warnings:
+ construct = _name_construct(tokens[i])
+ count, state = _policy_string(policy)
+ self.add_message('C0326', line=tokens[i][2][0],
+ args=(count, state, position, construct,
+ _underline_token(tokens[i])))
+
+ def _inside_brackets(self, left):
+ return self._bracket_stack[-1] == left
+
+ def _prepare_token_dispatcher(self):
+ raw = [
+ (_KEYWORD_TOKENS,
+ self._check_keyword_parentheses),
+
+ (_OPENING_BRACKETS, self._opening_bracket),
+
+ (_CLOSING_BRACKETS, self._closing_bracket),
+
+ (['='], self._check_equals_spacing),
+
+ (_SPACED_OPERATORS, self._check_surrounded_by_space),
+
+ ([','], self._handle_comma),
+
+ ([':'], self._handle_colon),
+
+ (['lambda'], self._open_lambda),
+ ]
+
+ dispatch = {}
+ for tokens, handler in raw:
+ for token in tokens:
+ dispatch[token] = handler
+ return dispatch
+
def process_tokens(self, tokens):
"""process tokens and search for :
@@ -222,6 +418,7 @@ class FormatChecker(BaseTokenChecker):
_ optionally bad construct (if given, bad_construct must be a compiled
regular expression).
"""
+ self._bracket_stack = [None]
indent = tokenize.INDENT
dedent = tokenize.DEDENT
newline = tokenize.NEWLINE
@@ -233,7 +430,8 @@ class FormatChecker(BaseTokenChecker):
self._lines = {}
self._visited_lines = {}
new_line_delay = False
- for (tok_type, token, start, _, line) in tokens:
+ token_handlers = self._prepare_token_dispatcher()
+ 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)
@@ -292,11 +490,18 @@ class FormatChecker(BaseTokenChecker):
check_equal = 0
self.check_indent_level(line, indents[-1], line_num)
+ try:
+ handler = token_handlers[token]
+ except KeyError:
+ pass
+ else:
+ handler(tokens, idx)
+
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)
- @check_messages('C0321' ,'C03232', 'C0323', 'C0324')
+ @check_messages('C0321', 'C03232', 'C0323', 'C0324')
def visit_default(self, node):
"""check the node line number and check it if not yet done"""
if not node.is_statement:
@@ -307,16 +512,19 @@ class FormatChecker(BaseTokenChecker):
if prev_sibl is not None:
prev_line = prev_sibl.fromlineno
else:
- prev_line = node.parent.statement().fromlineno
+ # The line on which a finally: occurs in a try/finally
+ # is not directly represented in the AST. We infer it
+ # 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):
+ prev_line = node.parent.body[0].tolineno + 1
+ else:
+ prev_line = node.parent.statement().fromlineno
line = node.fromlineno
assert line, node
if prev_line == line and self._visited_lines.get(line) != 2:
- # py2.5 try: except: finally:
- if not (isinstance(node, nodes.TryExcept)
- and isinstance(node.parent, nodes.TryFinally)
- and node.fromlineno == node.parent.fromlineno):
- self.add_message('C0321', node=node)
- self._visited_lines[line] = 2
+ self._check_multi_statement_line(node, line)
return
if line in self._visited_lines:
return
@@ -332,13 +540,23 @@ class FormatChecker(BaseTokenChecker):
lines.append(self._lines[line].rstrip())
except KeyError:
lines.append('')
- try:
- msg_def = check_line('\n'.join(lines))
- if msg_def:
- self.add_message(msg_def[0], node=node, args=msg_def[1])
- except KeyError:
- # FIXME: internal error !
- pass
+
+ def _check_multi_statement_line(self, node, line):
+ """Check for lines containing multiple statements."""
+ # Do not warn about multiple nested context managers
+ # in with statements.
+ if isinstance(node, nodes.With):
+ return
+ # 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)):
+ return
+ 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._visited_lines[line] = 2
@check_messages('W0333')
def visit_backquote(self, node):
@@ -388,7 +606,7 @@ class FormatChecker(BaseTokenChecker):
self.add_message('W0312', args=args, line=line_num)
return level
suppl += string[0]
- string = string [1:]
+ string = string[1:]
if level != expected or suppl:
i_type = 'spaces'
if indent[0] == '\t':
diff --git a/checkers/imports.py b/checkers/imports.py
index 1dd7787..b0a9872 100644
--- a/checkers/imports.py
+++ b/checkers/imports.py
@@ -93,7 +93,7 @@ def dependencies_graph(filename, dep_info):
"""write dependencies as a dot (graphviz) file
"""
done = {}
- printer = DotBackend(filename[:-4], rankdir = "LR")
+ printer = DotBackend(filename[:-4], rankdir='LR')
printer.emit('URL="." node[shape="box"]')
for modname, dependencies in sorted(dep_info.iteritems()):
done[modname] = 1
@@ -301,11 +301,10 @@ given file (report RP0402 must not be disabled)'}
importedmodname, set())
if not context_name in importedmodnames:
importedmodnames.add(context_name)
- if is_standard_module(importedmodname, (self.package_dir(),)):
- # update import graph
- mgraph = self.import_graph.setdefault(context_name, set())
- if not importedmodname in mgraph:
- mgraph.add(importedmodname)
+ # update import graph
+ mgraph = self.import_graph.setdefault(context_name, set())
+ if not importedmodname in mgraph:
+ mgraph.add(importedmodname)
def _check_deprecated_module(self, node, mod_path):
"""check if the module is deprecated"""
diff --git a/checkers/logging.py b/checkers/logging.py
index 6986ca4..d1f9d36 100644
--- a/checkers/logging.py
+++ b/checkers/logging.py
@@ -91,7 +91,7 @@ class LoggingChecker(checkers.BaseChecker):
and ancestor.parent.name == 'logging')))]
except astroid.exceptions.InferenceError:
return
- if (node.func.expr.name != self._logging_name and not logger_class):
+ if node.func.expr.name != self._logging_name and not logger_class:
return
self._check_convenience_methods(node)
self._check_log_methods(node)
@@ -129,7 +129,7 @@ class LoggingChecker(checkers.BaseChecker):
node: AST node to be checked.
format_arg: Index of the format string in the node arguments.
"""
- num_args = self._count_supplied_tokens(node.args[format_arg + 1:])
+ num_args = _count_supplied_tokens(node.args[format_arg + 1:])
if not num_args:
# If no args were supplied, then all format strings are valid -
# don't check any further.
@@ -147,9 +147,10 @@ class LoggingChecker(checkers.BaseChecker):
# Keyword checking on logging strings is complicated by
# special keywords - out of scope.
return
- except utils.UnsupportedFormatCharacter, e:
- c = format_string[e.index]
- self.add_message('E1200', node=node, args=(c, ord(c), e.index))
+ except utils.UnsupportedFormatCharacter, ex:
+ char = format_string[ex.index]
+ self.add_message('E1200', node=node,
+ args=(char, ord(char), ex.index))
return
except utils.IncompleteFormatString:
self.add_message('E1201', node=node)
@@ -159,20 +160,21 @@ class LoggingChecker(checkers.BaseChecker):
elif num_args < required_num_args:
self.add_message('E1206', node=node)
- def _count_supplied_tokens(self, args):
- """Counts the number of tokens in an args list.
- The Python log functions allow for special keyword arguments: func,
- exc_info and extra. To handle these cases correctly, we only count
- arguments that aren't keywords.
+def _count_supplied_tokens(args):
+ """Counts the number of tokens in an args list.
- Args:
- args: List of AST nodes that are arguments for a log format string.
+ The Python log functions allow for special keyword arguments: func,
+ exc_info and extra. To handle these cases correctly, we only count
+ arguments that aren't keywords.
- Returns:
- Number of AST nodes that aren't keywords.
- """
- return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))
+ Args:
+ args: List of AST nodes that are arguments for a log format string.
+
+ Returns:
+ Number of AST nodes that aren't keywords.
+ """
+ return sum(1 for arg in args if not isinstance(arg, astroid.Keyword))
def register(linter):
diff --git a/checkers/newstyle.py b/checkers/newstyle.py
index 9832195..ff9bbc2 100644
--- a/checkers/newstyle.py
+++ b/checkers/newstyle.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2005-2006 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2005-2013 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
@@ -37,7 +37,8 @@ MSGS = {
'E1004': ('Missing argument to super()',
'missing-super-argument',
'Used when the super builtin didn\'t receive an \
- argument on Python 2'),
+ argument on Python 2',
+ {'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" \
diff --git a/checkers/raw_metrics.py b/checkers/raw_metrics.py
index a8e4367..23e45b0 100644
--- a/checkers/raw_metrics.py
+++ b/checkers/raw_metrics.py
@@ -68,11 +68,11 @@ class RawMetricsChecker(BaseTokenChecker):
# configuration section name
name = 'metrics'
# configuration options
- options = ( )
+ options = ()
# messages
msgs = {}
# reports
- reports = ( ('RP0701', 'Raw metrics', report_raw_stats), )
+ reports = (('RP0701', 'Raw metrics', report_raw_stats),)
def __init__(self, linter):
BaseTokenChecker.__init__(self, linter)
diff --git a/checkers/similar.py b/checkers/similar.py
index 26b3725..8d755fa 100644
--- a/checkers/similar.py
+++ b/checkers/similar.py
@@ -63,15 +63,15 @@ class Similar(object):
duplicate = no_duplicates.setdefault(num, [])
for couples in duplicate:
if (lineset1, idx1) in couples or (lineset2, idx2) in couples:
- couples.add( (lineset1, idx1) )
- couples.add( (lineset2, idx2) )
+ couples.add((lineset1, idx1))
+ couples.add((lineset2, idx2))
break
else:
- duplicate.append( set([(lineset1, idx1), (lineset2, idx2)]) )
+ duplicate.append(set([(lineset1, idx1), (lineset2, idx2)]))
sims = []
for num, ensembles in no_duplicates.iteritems():
for couples in ensembles:
- sims.append( (num, couples) )
+ sims.append((num, couples))
sims.sort()
sims.reverse()
return sims
@@ -104,7 +104,7 @@ class Similar(object):
while index1 < len(lineset1):
skip = 1
num = 0
- for index2 in find( lineset1[index1] ):
+ for index2 in find(lineset1[index1]):
non_blank = 0
for num, ((_, line1), (_, line2)) in enumerate(
izip(lines1(index1), lines2(index2))):
@@ -210,7 +210,7 @@ class LineSet(object):
index = {}
for line_no, line in enumerate(self._stripped_lines):
if line:
- index.setdefault(line, []).append( line_no )
+ index.setdefault(line, []).append(line_no)
return index
@@ -260,7 +260,7 @@ class SimilarChecker(BaseChecker, Similar):
),
)
# reports
- reports = ( ('RP0801', 'Duplication', report_similarities), )
+ reports = (('RP0801', 'Duplication', report_similarities),)
def __init__(self, linter=None):
BaseChecker.__init__(self, linter)
@@ -349,9 +349,9 @@ def Run(argv=None):
usage()
elif opt in ('-i', '--ignore-comments'):
ignore_comments = True
- elif opt in ('--ignore-docstrings'):
+ elif opt in ('--ignore-docstrings',):
ignore_docstrings = True
- elif opt in ('--ignore-imports'):
+ elif opt in ('--ignore-imports',):
ignore_imports = True
if not args:
usage(1)
diff --git a/checkers/stdlib.py b/checkers/stdlib.py
index 07e1fbe..b63760c 100644
--- a/checkers/stdlib.py
+++ b/checkers/stdlib.py
@@ -21,7 +21,7 @@ import sys
import astroid
from pylint.interfaces import IAstroidChecker
-from pylint.checkers import BaseChecker, BaseTokenChecker
+from pylint.checkers import BaseChecker
from pylint.checkers import utils
_VALID_OPEN_MODE_REGEX = r'^(r?U|[rwa]\+?b?)$'
diff --git a/checkers/strings.py b/checkers/strings.py
index 42563da..c6bf960 100644
--- a/checkers/strings.py
+++ b/checkers/strings.py
@@ -66,11 +66,11 @@ MSGS = {
'E1305': ("Too many arguments for format string",
"too-many-format-args",
"Used when a format string that uses unnamed conversion \
- specifiers is given too few arguments."),
+ specifiers is given too many arguments."),
'E1306': ("Not enough arguments for format string",
"too-few-format-args",
"Used when a format string that uses unnamed conversion \
- specifiers is given too many arguments"),
+ specifiers is given too few arguments"),
}
OTHER_NODES = (astroid.Const, astroid.List, astroid.Backquote,
@@ -233,7 +233,7 @@ class StringConstantChecker(BaseTokenChecker):
if c in '\'\"':
quote_char = c
break
- prefix = token[:i].lower() # markers like u, b, r.
+ prefix = token[:i].lower() # markers like u, b, r.
after_prefix = token[i:]
if after_prefix[:3] == after_prefix[-3:] == 3 * quote_char:
string_body = after_prefix[3:-3]
diff --git a/checkers/typecheck.py b/checkers/typecheck.py
index 6988359..2e3785e 100644
--- a/checkers/typecheck.py
+++ b/checkers/typecheck.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2006-2010 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2006-2013 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
@@ -292,7 +292,7 @@ accessed. Python regular expressions are accepted.'}
# Built-in functions have no argument information.
return
- if len( called.argnames() ) != len( set( called.argnames() ) ):
+ if len(called.argnames()) != len(set(called.argnames())):
# Duplicate parameter name (see E9801). We can't really make sense
# of the function call in this case, so just return.
return
diff --git a/checkers/utils.py b/checkers/utils.py
index 5e028f2..7387711 100644
--- a/checkers/utils.py
+++ b/checkers/utils.py
@@ -60,7 +60,7 @@ def clobber_in_except(node):
(False, None) otherwise.
"""
if isinstance(node, astroid.AssAttr):
- return (True, (node.attrname, 'object %r' % (node.expr.name,)))
+ return (True, (node.attrname, 'object %r' % (node.expr.as_string(),)))
elif isinstance(node, astroid.AssName):
name = node.name
if is_builtin(name):
@@ -69,8 +69,9 @@ def clobber_in_except(node):
scope, stmts = node.lookup(name)
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,)))
+ (astroid.Assign, astroid.AugAssign,
+ astroid.ExceptHandler))):
+ return (True, (name, 'outer scope (line %s)' % stmts[0].fromlineno))
return (False, None)
@@ -153,8 +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
- if vars and vars.name == varname:
+ break
+ if (vars and
+ isinstance(vars, astroid.AssName) and
+ vars.name == varname):
return True
elif isinstance(_node, (astroid.Lambda, astroid.Function)):
if _node.args.is_argument(varname):
@@ -162,6 +165,11 @@ def is_defined_before(var_node):
if getattr(_node, 'name', None) == varname:
return True
break
+ elif isinstance(_node, astroid.ExceptHandler):
+ if isinstance(_node.name, astroid.AssName):
+ ass_node = _node.name
+ if ass_node.name == varname:
+ return True
_node = _node.parent
# possibly multiple statements on the same line using semi colon separator
stmt = var_node.statement()
@@ -171,7 +179,7 @@ def is_defined_before(var_node):
for ass_node in _node.nodes_of_class(astroid.AssName):
if ass_node.name == varname:
return True
- for imp_node in _node.nodes_of_class( (astroid.From, astroid.Import)):
+ for imp_node in _node.nodes_of_class((astroid.From, astroid.Import)):
if varname in [name[1] or name[0] for name in imp_node.names]:
return True
_node = _node.previous_sibling()
@@ -296,52 +304,52 @@ def parse_format_string(format_string):
return (i, format_string[i])
i = 0
while i < len(format_string):
- c = format_string[i]
- if c == '%':
- i, c = next_char(i)
+ char = format_string[i]
+ if char == '%':
+ i, char = next_char(i)
# Parse the mapping key (optional).
key = None
- if c == '(':
+ if char == '(':
depth = 1
- i, c = next_char(i)
+ i, char = next_char(i)
key_start = i
while depth != 0:
- if c == '(':
+ if char == '(':
depth += 1
- elif c == ')':
+ elif char == ')':
depth -= 1
- i, c = next_char(i)
+ i, char = next_char(i)
key_end = i - 1
key = format_string[key_start:key_end]
# Parse the conversion flags (optional).
- while c in '#0- +':
- i, c = next_char(i)
+ while char in '#0- +':
+ i, char = next_char(i)
# Parse the minimum field width (optional).
- if c == '*':
+ if char == '*':
num_args += 1
- i, c = next_char(i)
+ i, char = next_char(i)
else:
- while c in string.digits:
- i, c = next_char(i)
+ while char in string.digits:
+ i, char = next_char(i)
# Parse the precision (optional).
- if c == '.':
- i, c = next_char(i)
- if c == '*':
+ if char == '.':
+ i, char = next_char(i)
+ if char == '*':
num_args += 1
- i, c = next_char(i)
+ i, char = next_char(i)
else:
- while c in string.digits:
- i, c = next_char(i)
+ while char in string.digits:
+ i, char = next_char(i)
# Parse the length modifier (optional).
- if c in 'hlL':
- i, c = next_char(i)
+ if char in 'hlL':
+ i, char = next_char(i)
# Parse the conversion type (mandatory).
- if c not in 'diouxXeEfFgGcrs%':
+ if char not in 'diouxXeEfFgGcrs%':
raise UnsupportedFormatCharacter(i)
if key:
keys.add(key)
- elif c != '%':
+ elif char != '%':
num_args += 1
i += 1
return keys, num_args
diff --git a/checkers/variables.py b/checkers/variables.py
index 0d35884..90b7fe7 100644
--- a/checkers/variables.py
+++ b/checkers/variables.py
@@ -51,6 +51,20 @@ def overridden_method(klass, name):
return meth_node
return None
+def _get_unpacking_extra_info(node, infered):
+ """return extra information to add to the message for unpacking-non-sequence
+ and unbalanced-tuple-unpacking errors
+ """
+ more = ''
+ infered_module = infered.root().name
+ if node.root().name == infered_module:
+ if node.lineno == infered.lineno:
+ more = ' %s' % infered.as_string()
+ elif infered.lineno:
+ more = ' defined at line %s' % infered.lineno
+ elif infered.lineno:
+ more = ' defined at line %s of %s' % (infered.lineno, infered_module)
+ return more
MSGS = {
'E0601': ('Using variable %r before assignment',
@@ -120,13 +134,12 @@ MSGS = {
the loop.'),
'W0632': ('Possible unbalanced tuple unpacking with '
- 'sequence at line %s: '
+ 'sequence%s: '
'left side has %d label(s), right side has %d value(s)',
'unbalanced-tuple-unpacking',
'Used when there is an unbalanced tuple unpacking in assignment'),
- 'W0633': ('Attempting to unpack a non-sequence with '
- 'non-sequence at line %s',
+ 'W0633': ('Attempting to unpack a non-sequence%s',
'unpacking-non-sequence',
'Used when something which is not '
'a sequence is used in an unpack assignment'),
@@ -556,7 +569,7 @@ builtins. Remember that you should avoid to define new builtins when possible.'
"""
if not isinstance(node.targets[0], (astroid.Tuple, astroid.List)):
return
-
+
targets = node.targets[0].itered()
if any(not isinstance(target_node, astroid.AssName)
for target_node in targets):
@@ -572,41 +585,30 @@ builtins. Remember that you should avoid to define new builtins when possible.'
""" Check for unbalanced tuple unpacking
and unpacking non sequences.
"""
- if isinstance(infered, (astroid.Tuple, astroid.List)):
+ if infered is astroid.YES:
+ return
+ if isinstance(infered, (astroid.Tuple, astroid.List)):
+ # attempt to check unpacking is properly balanced
values = infered.itered()
if len(targets) != len(values):
- if node.root().name == infered.root().name:
- location = infered.lineno or 'unknown'
- else:
- location = '%s (%s)' % (infered.lineno or 'unknown',
- infered.root().name)
-
- self.add_message('unbalanced-tuple-unpacking',
- node=node,
- args=(location,
- len(targets),
+ self.add_message('unbalanced-tuple-unpacking', node=node,
+ args=(_get_unpacking_extra_info(node, infered),
+ len(targets),
len(values)))
- else:
- if infered is astroid.YES:
- return
-
+ # attempt to check unpacking may be possible (ie RHS is iterable)
+ elif isinstance(infered, astroid.Instance):
for meth in ('__iter__', '__getitem__'):
try:
infered.getattr(meth)
+ break
except astroid.NotFoundError:
continue
- else:
- break
- else:
- if node.root().name == infered.root().name:
- location = infered.lineno or 'unknown'
- else:
- location = '%s (%s)' % (infered.lineno or 'unknown',
- infered.root().name)
-
- self.add_message('unpacking-non-sequence',
- node=node,
- args=(location, ))
+ else:
+ self.add_message('unpacking-non-sequence', node=node,
+ args=(_get_unpacking_extra_info(node, infered),))
+ else:
+ self.add_message('unpacking-non-sequence', node=node,
+ args=(_get_unpacking_extra_info(node, infered),))
def _check_module_attrs(self, node, module, module_names):