summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichal Nowikowski <godfryd@gmail.com>2014-07-25 13:37:25 +0200
committerMichal Nowikowski <godfryd@gmail.com>2014-07-25 13:37:25 +0200
commitaf2dbfc0d0952dfe4f039627101d5d60fb415882 (patch)
tree20b19b2df4723b05a82392269cf211620117cccc
parentde0f498b20df4cf9cb540106a5dd98117fb61fd0 (diff)
parentee26e69c938d62c3fc48cd6848bee35ccce70637 (diff)
downloadpylint-af2dbfc0d0952dfe4f039627101d5d60fb415882.tar.gz
Merged logilab/pylint into default
-rw-r--r--checkers/format.py35
-rw-r--r--checkers/imports.py4
-rw-r--r--checkers/typecheck.py3
-rw-r--r--checkers/variables.py10
-rw-r--r--lint.py122
-rw-r--r--reporters/__init__.py2
-rw-r--r--test/functional/abstract_abc_methods.args3
-rw-r--r--test/functional/abstract_abc_methods.py (renamed from test/input/func_noerror_abstract_method.py)5
-rw-r--r--test/functional/anomalous_unicode_escape.args2
-rw-r--r--test/functional/anomalous_unicode_escape.py (renamed from test/input/func_raw_escapes.py)5
-rw-r--r--test/functional/anomalous_unicode_escape.txt3
-rw-r--r--test/functional/future_unicode_literals.args2
-rw-r--r--test/functional/future_unicode_literals.py (renamed from test/input/func_unicode_literal.py)3
-rw-r--r--test/functional/future_unicode_literals.txt1
-rw-r--r--test/functional/name_styles.args5
-rw-r--r--test/functional/name_styles.py117
-rw-r--r--test/functional/name_styles.txt17
-rw-r--r--test/functional/namedtuple_member_inference.args2
-rw-r--r--test/functional/namedtuple_member_inference.py21
-rw-r--r--test/functional/namedtuple_member_inference.txt3
-rw-r--r--test/functional/suspicious_str_strip_call.args3
-rw-r--r--test/functional/suspicious_str_strip_call.py9
-rw-r--r--test/functional/suspicious_str_strip_call.txt3
-rw-r--r--test/functional/suspicious_str_strip_call_py3.args2
-rw-r--r--test/functional/suspicious_str_strip_call_py3.py9
-rw-r--r--test/functional/suspicious_str_strip_call_py3.txt3
-rw-r--r--test/input/func_bad_continuation.py4
-rw-r--r--test/input/func_bad_str_strip_call.py9
-rw-r--r--test/input/func_ctor_arguments.py7
-rw-r--r--test/input/func_loopvar_in_closure.py18
-rw-r--r--test/input/func_name_checking.py135
-rw-r--r--test/input/func_namedtuple.py10
-rw-r--r--test/input/func_noerror_abstract_method_py30.py19
-rw-r--r--test/input/func_w0404.py6
-rw-r--r--test/messages/func_bad_str_strip_call.txt3
-rw-r--r--test/messages/func_bad_str_strip_call_py30.txt3
-rw-r--r--test/messages/func_i0022.txt9
-rw-r--r--test/messages/func_loopvar_in_closure.txt12
-rw-r--r--test/messages/func_name_checking.txt18
-rw-r--r--test/messages/func_namedtuple.txt1
-rw-r--r--test/messages/func_w0404.txt2
-rw-r--r--test/test_func.py8
-rw-r--r--test/test_functional.py21
-rw-r--r--test/test_regr.py4
-rw-r--r--test/unittest_checker_base.py4
-rw-r--r--test/unittest_checker_classes.py1
-rw-r--r--test/unittest_checker_format.py1
-rw-r--r--test/unittest_checker_logging.py1
-rw-r--r--test/unittest_checker_misc.py2
-rw-r--r--test/unittest_checker_typecheck.py1
-rw-r--r--test/unittest_checker_variables.py1
-rw-r--r--test/unittest_checkers_utils.py1
-rw-r--r--test/unittest_lint.py219
-rw-r--r--test/unittest_reporting.py2
-rw-r--r--testutils.py28
-rw-r--r--utils.py359
56 files changed, 711 insertions, 592 deletions
diff --git a/checkers/format.py b/checkers/format.py
index 8b73049..c07ff32 100644
--- a/checkers/format.py
+++ b/checkers/format.py
@@ -336,16 +336,18 @@ class ContinuedLineState(object):
_BeforeBlockOffsets(indentation + self._continuation_size,
indentation + self._continuation_size * 2))
elif bracket == ':':
- if self._cont_stack[-1].context_type == CONTINUED:
- # If the dict key was on the same line as the open brace, the new
- # correct indent should be relative to the key instead of the
- # current indent level
- paren_align = self._cont_stack[-1].valid_outdent_offsets
- next_align = self._cont_stack[-1].valid_continuation_offsets.copy()
- next_align[next_align.keys()[0] + self._continuation_size] = True
- else:
- next_align = _Offsets(indentation + self._continuation_size, indentation)
- paren_align = _Offsets(indentation + self._continuation_size, indentation)
+ # If the dict key was on the same line as the open brace, the new
+ # correct indent should be relative to the key instead of the
+ # current indent level
+ paren_align = self._cont_stack[-1].valid_outdent_offsets
+ next_align = self._cont_stack[-1].valid_continuation_offsets.copy()
+ next_align[next_align.keys()[0] + self._continuation_size] = True
+ # Note that the continuation of
+ # d = {
+ # 'a': 'b'
+ # 'c'
+ # }
+ # is handled by the special-casing for hanging continued string indents.
return _ContinuedIndent(HANGING_DICT_VALUE, bracket, position, paren_align, next_align)
else:
return _ContinuedIndent(
@@ -793,16 +795,19 @@ class FormatChecker(BaseTokenChecker):
self._add_continuation_message(state, hints, tokens, indent_pos)
def _check_continued_indentation(self, tokens, next_idx):
+ def same_token_around_nl(token_type):
+ return (tokens.type(next_idx) == token_type and
+ tokens.type(next_idx-2) == token_type)
+
# Do not issue any warnings if the next line is empty.
if not self._current_line.has_content or tokens.type(next_idx) == tokenize.NL:
return
state, valid_offsets = self._current_line.get_valid_offsets(next_idx)
- # Special handling for hanging comments. If the last line ended with a
- # comment and the new line contains only a comment, the line may also be
- # indented to the start of the previous comment.
- if (tokens.type(next_idx) == tokenize.COMMENT and
- tokens.type(next_idx-2) == tokenize.COMMENT):
+ # Special handling for hanging comments and strings. If the last line ended
+ # with a comment (string) and the new line contains only a comment, the line
+ # may also be indented to the start of the previous token.
+ if same_token_around_nl(tokenize.COMMENT) or same_token_around_nl(tokenize.STRING):
valid_offsets[tokens.start_col(next_idx-2)] = True
# We can only decide if the indentation of a continued line before opening
diff --git a/checkers/imports.py b/checkers/imports.py
index 7194134..9ae3935 100644
--- a/checkers/imports.py
+++ b/checkers/imports.py
@@ -372,7 +372,7 @@ given file (report RP0402 must not be disabled)'}
cache them
"""
if self.__ext_dep_info is None:
- package = self.linter.base_name
+ package = self.linter.current_name
self.__ext_dep_info = result = {}
for importee, importers in self.stats['dependencies'].iteritems():
if not importee.startswith(package):
@@ -384,7 +384,7 @@ given file (report RP0402 must not be disabled)'}
cache them
"""
if self.__int_dep_info is None:
- package = self.linter.base_name
+ package = self.linter.current_name
self.__int_dep_info = result = {}
for importee, importers in self.stats['dependencies'].iteritems():
if importee.startswith(package):
diff --git a/checkers/typecheck.py b/checkers/typecheck.py
index 3bcb099..efc0909 100644
--- a/checkers/typecheck.py
+++ b/checkers/typecheck.py
@@ -386,7 +386,8 @@ accessed. Python regular expressions are accepted.'}
if isinstance(arg, astroid.Keyword):
keyword = arg.arg
if keyword in keyword_args:
- self.add_message('duplicate-keyword-arg', node=node, args=keyword)
+ self.add_message('duplicate-keyword-arg', node=node,
+ args=(keyword, 'function'))
keyword_args.add(keyword)
else:
num_positional_args += 1
diff --git a/checkers/variables.py b/checkers/variables.py
index 8f8ee87..3b9bcda 100644
--- a/checkers/variables.py
+++ b/checkers/variables.py
@@ -542,9 +542,15 @@ builtins. Remember that you should avoid to define new builtins when possible.'
self.add_message('global-statement', node=node)
def _check_late_binding_closure(self, node, assignment_node, scope_type):
+ def _is_direct_lambda_call():
+ return (isinstance(node_scope.parent, astroid.CallFunc)
+ and node_scope.parent.func is node_scope)
+
node_scope = node.scope()
if not isinstance(node_scope, (astroid.Lambda, astroid.Function)):
return
+ if isinstance(node.parent, astroid.Arguments):
+ return
if isinstance(assignment_node, astroid.Comprehension):
if assignment_node.parent.parent_of(node.scope()):
@@ -557,7 +563,9 @@ builtins. Remember that you should avoid to define new builtins when possible.'
break
maybe_for = maybe_for.parent
else:
- if maybe_for.parent_of(node_scope) and not isinstance(node_scope.statement(), astroid.Return):
+ if (maybe_for.parent_of(node_scope)
+ and not _is_direct_lambda_call()
+ and not isinstance(node_scope.statement(), astroid.Return)):
self.add_message('cell-var-from-loop', node=node, args=node.name)
def _loopvar_name(self, node, name):
diff --git a/lint.py b/lint.py
index 0b7b44f..4184985 100644
--- a/lint.py
+++ b/lint.py
@@ -50,7 +50,7 @@ from astroid.modutils import load_module_from_name, get_module_part
from pylint.utils import (
MSG_TYPES, OPTION_RGX,
PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn,
- EmptyReport, WarningScope,
+ MessagesStore, FileState, EmptyReport, WarningScope,
expand_modules, tokenize_module)
from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker
from pylint.checkers import (BaseTokenChecker,
@@ -131,7 +131,7 @@ MSGS = {
'deprecated-pragma',
'Some inline pylint options have been renamed or reworked, '
'only the most recent form should be used. '
- 'NOTE:skip-all is only available with pylint >= 0.26',
+ 'NOTE:skip-all is only available with pylint >= 0.26',
{'old_names': [('I0014', 'deprecated-disable-all')]}),
'E0001': ('%s',
@@ -286,15 +286,15 @@ warning, statement which respectively contain the number of errors / warnings\
pylintrc=None):
# some stuff has to be done before ancestors initialization...
#
- # checkers / reporter / astroid manager
+ # messages store / checkers / reporter / astroid manager
+ self.msgs_store = MessagesStore()
self.reporter = None
self._reporter_name = None
self._reporters = {}
self._checkers = {}
self._ignore_file = False
# visit variables
- self.base_name = None
- self.base_file = None
+ self.file_state = FileState()
self.current_name = None
self.current_file = None
self.stats = None
@@ -423,11 +423,11 @@ warning, statement which respectively contain the number of errors / warnings\
self.register_report(r_id, r_title, r_cb, checker)
self.register_options_provider(checker)
if hasattr(checker, 'msgs'):
- self.register_messages(checker)
+ self.msgs_store.register_messages(checker)
checker.load_defaults()
def disable_noerror_messages(self):
- for msgcat, msgids in self._msgs_by_category.iteritems():
+ for msgcat, msgids in self.msgs_store._msgs_by_category.iteritems():
if msgcat == 'E':
for msgid in msgids:
self.enable(msgid)
@@ -497,59 +497,6 @@ warning, statement which respectively contain the number of errors / warnings\
else:
self.add_message('unrecognized-inline-option', args=opt, line=start[0])
- def collect_block_lines(self, node, msg_state):
- """walk ast to collect block level options line numbers"""
- # recurse on children (depth first)
- for child in node.get_children():
- self.collect_block_lines(child, msg_state)
- first = node.fromlineno
- last = node.tolineno
- # first child line number used to distinguish between disable
- # which are the first child of scoped node with those defined later.
- # For instance in the code below:
- #
- # 1. def meth8(self):
- # 2. """test late disabling"""
- # 3. # pylint: disable=E1102
- # 4. print self.blip
- # 5. # pylint: disable=E1101
- # 6. print self.bla
- #
- # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
- #
- # this is necessary to disable locally messages applying to class /
- # function using their fromlineno
- if isinstance(node, (nodes.Module, nodes.Class, nodes.Function)) and node.body:
- firstchildlineno = node.body[0].fromlineno
- else:
- firstchildlineno = last
- for msgid, lines in msg_state.iteritems():
- for lineno, state in lines.items():
- original_lineno = lineno
- if first <= lineno <= last:
- # Set state for all lines for this block, if the
- # warning is applied to nodes.
- if self.check_message_id(msgid).scope == WarningScope.NODE:
- if lineno > firstchildlineno:
- state = True
- first_, last_ = node.block_range(lineno)
- else:
- first_ = lineno
- last_ = last
- for line in xrange(first_, last_+1):
- # do not override existing entries
- if not line in self._module_msgs_state.get(msgid, ()):
- if line in lines: # state change in the same block
- state = lines[line]
- original_lineno = line
- if not state:
- self._suppression_mapping[(msgid, line)] = original_lineno
- try:
- self._module_msgs_state[msgid][line] = state
- except KeyError:
- self._module_msgs_state[msgid] = {line: state}
- del lines[lineno]
-
# code checking methods ###################################################
@@ -570,7 +517,7 @@ warning, statement which respectively contain the number of errors / warnings\
if msg[0] != 'F' and self.is_message_enabled(msg))
if (messages or
any(self.report_is_enabled(r[0]) for r in checker.reports)):
- neededcheckers.append(checker)
+ neededcheckers.append(checker)
# Sort checkers by priority
neededcheckers = sorted(neededcheckers, key=attrgetter('priority'),
reverse=True)
@@ -596,6 +543,12 @@ warning, statement which respectively contain the number of errors / warnings\
"""main checking entry: check a list of files or modules from their
name.
"""
+ # initialize msgs_state now that all messages have been registered into
+ # the store
+ for msg in self.msgs_store.messages:
+ if not msg.may_be_emitted():
+ self._msgs_state[msg.msgid] = False
+
if not isinstance(files_or_modules, (list, tuple)):
files_or_modules = (files_or_modules,)
walker = PyLintASTWalker(self)
@@ -621,14 +574,18 @@ warning, statement which respectively contain the number of errors / warnings\
astroid = self.get_ast(filepath, modname)
if astroid is None:
continue
- self.base_name = descr['basename']
- self.base_file = descr['basepath']
+ # XXX to be correct we need to keep module_msgs_state for every
+ # analyzed module (the problem stands with localized messages which
+ # are only detected in the .close step)
+ self.file_state = FileState(descr['basename'])
self._ignore_file = False
# fix the current file (if the source file was not available or
# if it's actually a c extension)
self.current_file = astroid.file # pylint: disable=maybe-no-member
self.check_astroid_module(astroid, walker, rawcheckers, tokencheckers)
- self._add_suppression_messages()
+ # warn about spurious inline messages handling
+ for msgid, line, args in self.file_state.iter_spurious_suppression_messages(self.msgs_store):
+ self.add_message(msgid, line, None, args)
# notify global end
self.set_current_module('')
self.stats['statement'] = walker.nbstatements
@@ -662,13 +619,6 @@ warning, statement which respectively contain the number of errors / warnings\
self.stats['by_module'][modname]['statement'] = 0
for msg_cat in MSG_TYPES.itervalues():
self.stats['by_module'][modname][msg_cat] = 0
- # XXX hack, to be correct we need to keep module_msgs_state
- # for every analyzed module (the problem stands with localized
- # messages which are only detected in the .close step)
- if modname:
- self._module_msgs_state = {}
- self._raw_module_msgs_state = {}
- self._ignored_msgs = {}
def get_ast(self, filepath, modname):
"""return a ast(roid) representation for a module"""
@@ -702,12 +652,8 @@ warning, statement which respectively contain the number of errors / warnings\
if self._ignore_file:
return False
# walk ast to collect line numbers
- for msg, lines in self._module_msgs_state.iteritems():
- self._raw_module_msgs_state[msg] = lines.copy()
- orig_state = self._module_msgs_state.copy()
- self._module_msgs_state = {}
- self._suppression_mapping = {}
- self.collect_block_lines(astroid, orig_state)
+ self.file_state.collect_block_lines(self.msgs_store, astroid)
+ # run raw and tokens checkers
for checker in rawcheckers:
checker.process_module(astroid)
for checker in tokencheckers:
@@ -731,9 +677,9 @@ warning, statement which respectively contain the number of errors / warnings\
if persistent run, pickle results for later comparison
"""
- if self.base_name is not None:
+ if self.file_state.base_name is not None:
# load previous results if any
- previous_stats = config.load_results(self.base_name)
+ previous_stats = config.load_results(self.file_state.base_name)
# XXX code below needs refactoring to be more reporter agnostic
self.reporter.on_close(self.stats, previous_stats)
if self.config.reports:
@@ -747,24 +693,12 @@ warning, statement which respectively contain the number of errors / warnings\
self.reporter.display_results(sect)
# save results if persistent run
if self.config.persistent:
- config.save_results(self.stats, self.base_name)
+ config.save_results(self.stats, self.file_state.base_name)
else:
self.reporter.on_close(self.stats, {})
# specific reports ########################################################
- def _add_suppression_messages(self):
- for warning, lines in self._raw_module_msgs_state.iteritems():
- for line, enable in lines.iteritems():
- if not enable and (warning, line) not in self._ignored_msgs:
- self.add_message('useless-suppression', line, None,
- (self.get_msg_display_string(warning),))
- # don't use iteritems here, _ignored_msgs may be modified by add_message
- for (warning, from_), lines in self._ignored_msgs.items():
- for line in lines:
- self.add_message('suppressed-message', line, None,
- (self.get_msg_display_string(warning), from_))
-
def report_evaluation(self, sect, stats, previous_stats):
"""make the global evaluation report"""
# check with at least check 1 statements (usually 0 when there is a
@@ -1088,7 +1022,7 @@ are done by default'''}),
def cb_help_message(self, option, optname, value, parser):
"""optik callback for printing some help about a particular message"""
- self.linter.help_message(splitstrip(value))
+ self.linter.msgs_store.help_message(splitstrip(value))
sys.exit(0)
def cb_full_documentation(self, option, optname, value, parser):
@@ -1098,7 +1032,7 @@ are done by default'''}),
def cb_list_messages(self, option, optname, value, parser): # FIXME
"""optik callback for printing available messages"""
- self.linter.list_messages()
+ self.linter.msgs_store.list_messages()
sys.exit(0)
def cb_init_hook(optname, value):
diff --git a/reporters/__init__.py b/reporters/__init__.py
index a767a05..12d193f 100644
--- a/reporters/__init__.py
+++ b/reporters/__init__.py
@@ -51,7 +51,7 @@ class Message(object):
self.msg = msg
self.C = msg_id[0]
self.category = MSG_TYPES[msg_id[0]]
- self.symbol = reporter.linter.check_message_id(msg_id).symbol
+ self.symbol = reporter.linter.msgs_store.check_message_id(msg_id).symbol
def format(self, template):
"""Format the message according to the given template.
diff --git a/test/functional/abstract_abc_methods.args b/test/functional/abstract_abc_methods.args
new file mode 100644
index 0000000..2b38575
--- /dev/null
+++ b/test/functional/abstract_abc_methods.args
@@ -0,0 +1,3 @@
+[testoptions]
+min_pyver=2.6
+
diff --git a/test/input/func_noerror_abstract_method.py b/test/functional/abstract_abc_methods.py
index 18228c6..2e2bb13 100644
--- a/test/input/func_noerror_abstract_method.py
+++ b/test/functional/abstract_abc_methods.py
@@ -1,13 +1,10 @@
""" This should not warn about `prop` being abstract in Child """
-
# pylint: disable=too-few-public-methods,abstract-class-little-used
-__revision__ = None
-
import abc
class Parent(object):
- """ Class """
+ """Abstract Base Class """
__metaclass__ = abc.ABCMeta
@property
diff --git a/test/functional/anomalous_unicode_escape.args b/test/functional/anomalous_unicode_escape.args
new file mode 100644
index 0000000..8bfa6c0
--- /dev/null
+++ b/test/functional/anomalous_unicode_escape.args
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=2.6
diff --git a/test/input/func_raw_escapes.py b/test/functional/anomalous_unicode_escape.py
index b08b6f1..19a6912 100644
--- a/test/input/func_raw_escapes.py
+++ b/test/functional/anomalous_unicode_escape.py
@@ -2,8 +2,9 @@
"""Test for backslash escapes in byte vs unicode strings"""
# Would be valid in Unicode, but probably not what you want otherwise
-BAD_UNICODE = b'\u0042'
-BAD_LONG_UNICODE = b'\U00000042'
+BAD_UNICODE = b'\u0042' # [anomalous-unicode-escape-in-string]
+BAD_LONG_UNICODE = b'\U00000042' # [anomalous-unicode-escape-in-string]
+# +1:[anomalous-unicode-escape-in-string]
BAD_NAMED_UNICODE = b'\N{GREEK SMALL LETTER ALPHA}'
GOOD_UNICODE = u'\u0042'
diff --git a/test/functional/anomalous_unicode_escape.txt b/test/functional/anomalous_unicode_escape.txt
new file mode 100644
index 0000000..5c1acc7
--- /dev/null
+++ b/test/functional/anomalous_unicode_escape.txt
@@ -0,0 +1,3 @@
+anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix.
+anomalous-unicode-escape-in-string:6::Anomalous Unicode escape in byte string: '\U'. String constant might be missing an r or u prefix.
+anomalous-unicode-escape-in-string:8::Anomalous Unicode escape in byte string: '\N'. String constant might be missing an r or u prefix.
diff --git a/test/functional/future_unicode_literals.args b/test/functional/future_unicode_literals.args
new file mode 100644
index 0000000..d5fd0ce
--- /dev/null
+++ b/test/functional/future_unicode_literals.args
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=2.6 \ No newline at end of file
diff --git a/test/input/func_unicode_literal.py b/test/functional/future_unicode_literals.py
index fa47902..30c2bd6 100644
--- a/test/input/func_unicode_literal.py
+++ b/test/functional/future_unicode_literals.py
@@ -1,7 +1,6 @@
"""Unicode literals in Python 2.*"""
from __future__ import unicode_literals
-__revision__ = 0
-BAD_STRING = b'\u1234'
+BAD_STRING = b'\u1234' # >= 2.7.4:[anomalous-unicode-escape-in-string]
GOOD_STRING = '\u1234'
diff --git a/test/functional/future_unicode_literals.txt b/test/functional/future_unicode_literals.txt
new file mode 100644
index 0000000..c490da5
--- /dev/null
+++ b/test/functional/future_unicode_literals.txt
@@ -0,0 +1 @@
+anomalous-unicode-escape-in-string:5::Anomalous Unicode escape in byte string: '\u'. String constant might be missing an r or u prefix.
diff --git a/test/functional/name_styles.args b/test/functional/name_styles.args
new file mode 100644
index 0000000..1a63e67
--- /dev/null
+++ b/test/functional/name_styles.args
@@ -0,0 +1,5 @@
+[testoptions]
+min_pyver=2.6
+
+[Messages Control]
+disable=too-few-public-methods,abstract-class-not-used,global-statement
diff --git a/test/functional/name_styles.py b/test/functional/name_styles.py
new file mode 100644
index 0000000..be3e96d
--- /dev/null
+++ b/test/functional/name_styles.py
@@ -0,0 +1,117 @@
+"""Test for the invalid-name warning."""
+import abc
+import collections
+
+GOOD_CONST_NAME = ''
+bad_const_name = 0 # [invalid-name]
+
+
+def BADFUNCTION_name(): # [invalid-name]
+ """Bad function name."""
+ BAD_LOCAL_VAR = 1 # [invalid-name]
+ print BAD_LOCAL_VAR
+
+
+def func_bad_argname(NOT_GOOD): # [invalid-name]
+ """Function with a badly named argument."""
+ return NOT_GOOD
+
+
+def no_nested_args(arg1, arg21, arg22):
+ """Well-formed function."""
+ print arg1, arg21, arg22
+
+
+class bad_class_name(object): # [invalid-name]
+ """Class with a bad name."""
+
+
+class CorrectClassName(object):
+ """Class with a good name."""
+
+ def __init__(self):
+ self._good_private_name = 10
+ self.__good_real_private_name = 11
+ self.good_attribute_name = 12
+ self._Bad_AtTR_name = None # [invalid-name]
+ self.Bad_PUBLIC_name = None # [invalid-name]
+
+ zz = 'Bad Class Attribute' # [invalid-name]
+ GOOD_CLASS_ATTR = 'Good Class Attribute'
+
+ def BadMethodName(self): # [invalid-name]
+ """A Method with a bad name."""
+
+ def good_method_name(self):
+ """A method with a good name."""
+
+ def __DunDER_IS_not_free_for_all__(self): # [invalid-name]
+ """Another badly named method."""
+
+
+class DerivedFromCorrect(CorrectClassName):
+ """A derived class with an invalid inherited members.
+
+ Derived attributes and methods with invalid names do not trigger warnings.
+ """
+ zz = 'Now a good class attribute'
+
+ def __init__(self):
+ super(DerivedFromCorrect, self).__init__()
+ self._Bad_AtTR_name = None # Ignored
+
+ def BadMethodName(self):
+ """Ignored since the method is in the interface."""
+
+
+V = [WHAT_Ever_inListComp for WHAT_Ever_inListComp in GOOD_CONST_NAME]
+
+def class_builder():
+ """Function returning a class object."""
+
+ class EmbeddedClass(object):
+ """Useless class."""
+
+ return EmbeddedClass
+
+# +1:[invalid-name]
+BAD_NAME_FOR_CLASS = collections.namedtuple('Named', ['tuple'])
+NEXT_BAD_NAME_FOR_CLASS = class_builder() # [invalid-name]
+
+GoodName = collections.namedtuple('Named', ['tuple'])
+ToplevelClass = class_builder()
+
+# Aliases for classes have the same name constraints.
+AlsoCorrect = CorrectClassName
+NOT_CORRECT = CorrectClassName # [invalid-name]
+
+
+def test_globals():
+ """Names in global statements are also checked."""
+ global NOT_CORRECT
+ global AlsoCorrect # [invalid-name]
+ NOT_CORRECT = 1
+ AlsoCorrect = 2
+
+
+class FooClass(object):
+ """A test case for property names.
+
+ Since by default, the regex for attributes is the same as the one
+ for method names, we check the warning messages to contain the
+ string 'attribute'.
+ """
+ @property
+ def PROPERTY_NAME(self): # [invalid-name]
+ """Ignored."""
+ pass
+
+ @abc.abstractproperty
+ def ABSTRACT_PROPERTY_NAME(self): # [invalid-name]
+ """Ignored."""
+ pass
+
+ @PROPERTY_NAME.setter
+ def PROPERTY_NAME_SETTER(self): # [invalid-name]
+ """Ignored."""
+ pass
diff --git a/test/functional/name_styles.txt b/test/functional/name_styles.txt
new file mode 100644
index 0000000..4f0ae53
--- /dev/null
+++ b/test/functional/name_styles.txt
@@ -0,0 +1,17 @@
+invalid-name:6::Invalid constant name "bad_const_name"
+invalid-name:9:BADFUNCTION_name:Invalid function name "BADFUNCTION_name"
+invalid-name:11:BADFUNCTION_name:Invalid variable name "BAD_LOCAL_VAR"
+invalid-name:15:func_bad_argname:Invalid argument name "NOT_GOOD"
+invalid-name:25:bad_class_name:Invalid class name "bad_class_name"
+invalid-name:36:CorrectClassName.__init__:Invalid attribute name "_Bad_AtTR_name"
+invalid-name:37:CorrectClassName.__init__:Invalid attribute name "Bad_PUBLIC_name"
+invalid-name:39:CorrectClassName:Invalid class attribute name "zz"
+invalid-name:42:CorrectClassName.BadMethodName:Invalid method name "BadMethodName"
+invalid-name:48:CorrectClassName.__DunDER_IS_not_free_for_all__:Invalid method name "__DunDER_IS_not_free_for_all__"
+invalid-name:78::Invalid class name "BAD_NAME_FOR_CLASS"
+invalid-name:79::Invalid class name "NEXT_BAD_NAME_FOR_CLASS"
+invalid-name:86::Invalid class name "NOT_CORRECT"
+invalid-name:92:test_globals:Invalid constant name "AlsoCorrect"
+invalid-name:105:FooClass.PROPERTY_NAME:Invalid attribute name "PROPERTY_NAME"
+invalid-name:110:FooClass.ABSTRACT_PROPERTY_NAME:Invalid attribute name "ABSTRACT_PROPERTY_NAME"
+invalid-name:115:FooClass.PROPERTY_NAME_SETTER:Invalid attribute name "PROPERTY_NAME_SETTER"
diff --git a/test/functional/namedtuple_member_inference.args b/test/functional/namedtuple_member_inference.args
new file mode 100644
index 0000000..8bfa6c0
--- /dev/null
+++ b/test/functional/namedtuple_member_inference.args
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=2.6
diff --git a/test/functional/namedtuple_member_inference.py b/test/functional/namedtuple_member_inference.py
new file mode 100644
index 0000000..675c3e2
--- /dev/null
+++ b/test/functional/namedtuple_member_inference.py
@@ -0,0 +1,21 @@
+"""Test namedtuple attributes.
+
+Regression test for:
+https://bitbucket.org/logilab/pylint/issue/93/pylint-crashes-on-namedtuple-attribute
+"""
+__revision__ = None
+
+from collections import namedtuple
+Thing = namedtuple('Thing', ())
+
+Fantastic = namedtuple('Fantastic', ['foo'])
+
+def test():
+ """Test member access in named tuples."""
+ print Thing.x # [no-member]
+ fan = Fantastic(1)
+ print fan.foo
+ # Should not raise protected-access.
+ fan2 = fan._replace(foo=2) # [protected-access]
+ # This is a bug.
+ print fan2.foo # [no-member]
diff --git a/test/functional/namedtuple_member_inference.txt b/test/functional/namedtuple_member_inference.txt
new file mode 100644
index 0000000..87d9da4
--- /dev/null
+++ b/test/functional/namedtuple_member_inference.txt
@@ -0,0 +1,3 @@
+no-member:15:test:Class 'Thing' has no 'x' member
+protected-access:19:test:Access to a protected member _replace of a client class
+no-member:21:test:Instance of 'Fantastic' has no 'foo' member
diff --git a/test/functional/suspicious_str_strip_call.args b/test/functional/suspicious_str_strip_call.args
new file mode 100644
index 0000000..ecf5dcd
--- /dev/null
+++ b/test/functional/suspicious_str_strip_call.args
@@ -0,0 +1,3 @@
+[testoptions]
+min_pyver=2.6
+max_pyver=3.0
diff --git a/test/functional/suspicious_str_strip_call.py b/test/functional/suspicious_str_strip_call.py
new file mode 100644
index 0000000..e859f25
--- /dev/null
+++ b/test/functional/suspicious_str_strip_call.py
@@ -0,0 +1,9 @@
+"""Suspicious str.strip calls."""
+__revision__ = 1
+
+''.strip('yo')
+''.strip()
+
+u''.strip('http://') # [bad-str-strip-call]
+u''.lstrip('http://') # [bad-str-strip-call]
+b''.rstrip('http://') # [bad-str-strip-call]
diff --git a/test/functional/suspicious_str_strip_call.txt b/test/functional/suspicious_str_strip_call.txt
new file mode 100644
index 0000000..ad714cc
--- /dev/null
+++ b/test/functional/suspicious_str_strip_call.txt
@@ -0,0 +1,3 @@
+bad-str-strip-call:7::Suspicious argument in unicode.strip call
+bad-str-strip-call:8::Suspicious argument in unicode.lstrip call
+bad-str-strip-call:9::Suspicious argument in str.rstrip call
diff --git a/test/functional/suspicious_str_strip_call_py3.args b/test/functional/suspicious_str_strip_call_py3.args
new file mode 100644
index 0000000..c093be2
--- /dev/null
+++ b/test/functional/suspicious_str_strip_call_py3.args
@@ -0,0 +1,2 @@
+[testoptions]
+min_pyver=3.0
diff --git a/test/functional/suspicious_str_strip_call_py3.py b/test/functional/suspicious_str_strip_call_py3.py
new file mode 100644
index 0000000..e859f25
--- /dev/null
+++ b/test/functional/suspicious_str_strip_call_py3.py
@@ -0,0 +1,9 @@
+"""Suspicious str.strip calls."""
+__revision__ = 1
+
+''.strip('yo')
+''.strip()
+
+u''.strip('http://') # [bad-str-strip-call]
+u''.lstrip('http://') # [bad-str-strip-call]
+b''.rstrip('http://') # [bad-str-strip-call]
diff --git a/test/functional/suspicious_str_strip_call_py3.txt b/test/functional/suspicious_str_strip_call_py3.txt
new file mode 100644
index 0000000..81f32cf
--- /dev/null
+++ b/test/functional/suspicious_str_strip_call_py3.txt
@@ -0,0 +1,3 @@
+bad-str-strip-call:7::Suspicious argument in str.strip call
+bad-str-strip-call:8::Suspicious argument in str.lstrip call
+bad-str-strip-call:9::Suspicious argument in bytes.rstrip call
diff --git a/test/input/func_bad_continuation.py b/test/input/func_bad_continuation.py
index 03beaf4..f303915 100644
--- a/test/input/func_bad_continuation.py
+++ b/test/input/func_bad_continuation.py
@@ -185,3 +185,7 @@ if not (1 and
2): # [bad-continuation]
print 3
+continue2("foo",
+ some_other_arg="this "
+ "is "
+ "fine")
diff --git a/test/input/func_bad_str_strip_call.py b/test/input/func_bad_str_strip_call.py
deleted file mode 100644
index 2d94a6e..0000000
--- a/test/input/func_bad_str_strip_call.py
+++ /dev/null
@@ -1,9 +0,0 @@
-"""Suspicious str.strip calls."""
-__revision__ = 1
-
-''.strip('yo')
-''.strip()
-
-u''.strip('http://')
-u''.lstrip('http://')
-b''.rstrip('http://')
diff --git a/test/input/func_ctor_arguments.py b/test/input/func_ctor_arguments.py
index c708855..f9a3430 100644
--- a/test/input/func_ctor_arguments.py
+++ b/test/input/func_ctor_arguments.py
@@ -62,13 +62,16 @@ ClassNew(1, kwarg=1)
ClassNew(1, 2, 3)
ClassNew(one=2)
-import abc
+
+class Metaclass(type):
+ def __new__(mcs, name, bases, namespace):
+ return type.__new__(mcs, name, bases, namespace)
def with_metaclass(meta, base=object):
"""Create a new type that can be used as a metaclass."""
return meta("NewBase", (base, ), {})
-class ClassWithMeta(with_metaclass(abc.ABCMeta)):
+class ClassWithMeta(with_metaclass(Metaclass)):
pass
ClassWithMeta()
diff --git a/test/input/func_loopvar_in_closure.py b/test/input/func_loopvar_in_closure.py
index 32b7a6c..3a791d3 100644
--- a/test/input/func_loopvar_in_closure.py
+++ b/test/input/func_loopvar_in_closure.py
@@ -53,6 +53,23 @@ def good_case7():
return lambda: -1
+def good_case8():
+ """Lambda defined and called in loop."""
+ for i in range(10):
+ print (lambda x: i + x)(1)
+
+
+def good_case9():
+ """Another eager binding of the cell variable."""
+ funs = []
+ for i in range(10):
+ def func(bound_i=i):
+ """Ignore."""
+ return bound_i
+ funs.append(func)
+ return funs
+
+
def bad_case():
"""Closing over a loop variable."""
lst = []
@@ -112,3 +129,4 @@ def bad_case6():
print j
lst.append(lambda: i)
return lst
+
diff --git a/test/input/func_name_checking.py b/test/input/func_name_checking.py
deleted file mode 100644
index d19d946..0000000
--- a/test/input/func_name_checking.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# pylint: disable=R0903,R0201,R0921,W0603
-"""Test for the invalid-name (C0103) warning."""
-
-__revision__ = 1
-
-import collections
-
-def Run():
- """method without any good name"""
- class B(object):
- """nested class should not be tested has a variable"""
- def __init__(self):
- pass
- bBb = 1
- return A, bBb, B
-
-def run():
- """anothrer method without only good name"""
- class Aaa(object):
- """nested class should not be tested has a variable"""
- def __init__(self):
- pass
- bbb = 1
- return bbb, Aaa
-
-A = None
-
-def HOHOHOHO():
- """yo"""
- HIHIHI = 1
- print HIHIHI
-
-class xyz(object):
- """yo"""
-
- zz = 'Bad Class Attribute'
-
- def __init__(self):
- pass
-
- def Youplapoum(self):
- """bad method name"""
-
-
-class Derived(xyz):
- """Derived class."""
- zz = 'Not a bad class attribute'
-
-
-def no_nested_args(arg1, arg21, arg22):
- """a function which had nested arguments but no more"""
- print arg1, arg21, arg22
-
-
-GOOD_CONST_NAME = ''
-benpasceluila = 0
-
-class Correct(object):
- """yo"""
- def __init__(self):
- self.cava = 12
- self._Ca_va_Pas = None
-
- def BadMethodName(self):
- """Ignored."""
-
-V = [WHAT_Ever_inListComp for WHAT_Ever_inListComp in GOOD_CONST_NAME]
-
-def class_builder():
- """Function returning a class object."""
-
- class EmbeddedClass(object):
- """Useless class."""
-
- return EmbeddedClass
-
-BAD_NAME_FOR_CLASS = collections.namedtuple('Named', ['tuple'])
-NEXT_BAD_NAME_FOR_CLASS = class_builder()
-
-GoodName = collections.namedtuple('Named', ['tuple'])
-ToplevelClass = class_builder()
-
-AlsoCorrect = Correct
-NOT_CORRECT = Correct
-
-
-def test_globals():
- """Names in global statements are also checked."""
- global NOT_CORRECT
- global AlsoCorrect
- NOT_CORRECT = 1
- AlsoCorrect = 2
-
-
-class DerivedFromCorrect(Correct):
- """A derived class with an invalid inherited members.
-
- Derived attributes and methods with invalid names do not trigger warnings.
- """
-
- def __init__(self):
- super(DerivedFromCorrect, self).__init__()
- self._Ca_va_Pas = None
-
- def BadMethodName(self):
- """Ignored."""
-
-import abc
-
-class FooClass(object):
- """A test case for property names.
-
- Since by default, the regex for attributes is the same as the one
- for method names, we check the warning messages to contain the
- string 'attribute'.
- """
- @property
- def PROPERTY_NAME(self):
- """Ignored."""
- pass
-
- @abc.abstractproperty
- def ABSTRACT_PROPERTY_NAME(self):
- """Ignored."""
- pass
-
- @PROPERTY_NAME.setter
- def PROPERTY_NAME_SETTER(self):
- """Ignored."""
- pass
-
-
-def func_bad_argname(NOT_GOOD):
- """Function with a badly named argument."""
- return NOT_GOOD
diff --git a/test/input/func_namedtuple.py b/test/input/func_namedtuple.py
deleted file mode 100644
index 8cfd048..0000000
--- a/test/input/func_namedtuple.py
+++ /dev/null
@@ -1,10 +0,0 @@
-"""Test namedtuple attributes.
-
-Regression test for:
-https://bitbucket.org/logilab/pylint/issue/93/pylint-crashes-on-namedtuple-attribute
-"""
-__revision__ = None
-
-from collections import namedtuple
-Thing = namedtuple('Thing', ())
-print Thing.x
diff --git a/test/input/func_noerror_abstract_method_py30.py b/test/input/func_noerror_abstract_method_py30.py
deleted file mode 100644
index c237cb4..0000000
--- a/test/input/func_noerror_abstract_method_py30.py
+++ /dev/null
@@ -1,19 +0,0 @@
-""" This should not warn about `prop` being abstract in Child """
-
-# pylint: disable=too-few-public-methods,abstract-class-little-used,no-init,old-style-class
-
-__revision__ = None
-
-import abc
-
-class Parent(metaclass=abc.ABCMeta):
- """ Class """
-
- @property
- @abc.abstractmethod
- def prop(self):
- """ Abstract """
-
-class Child(Parent):
- """ No warning for the following. """
- prop = property(lambda self: 1)
diff --git a/test/input/func_w0404.py b/test/input/func_w0404.py
index 0e2f727..f475a4d 100644
--- a/test/input/func_w0404.py
+++ b/test/input/func_w0404.py
@@ -7,8 +7,8 @@ import sys
import xml.etree.ElementTree
from xml.etree import ElementTree
-from multiprocessing import pool
-import multiprocessing.pool
+from email import encoders
+import email.encoders
import sys
@@ -24,4 +24,4 @@ def reimport():
del sys
-del sys, ElementTree, xml.etree.ElementTree, pool, multiprocessing.pool
+del sys, ElementTree, xml.etree.ElementTree, encoders, email.encoders
diff --git a/test/messages/func_bad_str_strip_call.txt b/test/messages/func_bad_str_strip_call.txt
deleted file mode 100644
index 44b8780..0000000
--- a/test/messages/func_bad_str_strip_call.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-E: 7: Suspicious argument in unicode.strip call
-E: 8: Suspicious argument in unicode.lstrip call
-E: 9: Suspicious argument in str.rstrip call
diff --git a/test/messages/func_bad_str_strip_call_py30.txt b/test/messages/func_bad_str_strip_call_py30.txt
deleted file mode 100644
index 8ea0372..0000000
--- a/test/messages/func_bad_str_strip_call_py30.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-E: 7: Suspicious argument in str.strip call
-E: 8: Suspicious argument in str.lstrip call
-E: 9: Suspicious argument in bytes.rstrip call
diff --git a/test/messages/func_i0022.txt b/test/messages/func_i0022.txt
index ddcb066..fb527b1 100644
--- a/test/messages/func_i0022.txt
+++ b/test/messages/func_i0022.txt
@@ -1,12 +1,21 @@
I: 5: Locally disabling invalid-name (C0103)
I: 5: Suppressed 'invalid-name' (from line 5)
+I: 6: Locally disabling invalid-name (C0103)
I: 6: Pragma "disable-msg" is deprecated, use "disable" instead
I: 6: Suppressed 'invalid-name' (from line 6)
+I: 8: Locally disabling invalid-name (C0103)
I: 9: Suppressed 'invalid-name' (from line 8)
+I: 10: Locally enabling invalid-name (C0103)
+I: 12: Locally disabling invalid-name (C0103)
I: 12: Pragma "disable-msg" is deprecated, use "disable" instead
I: 13: Suppressed 'invalid-name' (from line 12)
+I: 14: Locally enabling invalid-name (C0103)
I: 14: Pragma "enable-msg" is deprecated, use "enable" instead
+I: 16: Locally disabling invalid-name (C0103)
I: 16: Pragma "disable-msg" is deprecated, use "disable" instead
I: 17: Suppressed 'invalid-name' (from line 16)
+I: 18: Locally enabling invalid-name (C0103)
I: 18: Pragma "enable-msg" is deprecated, use "enable" instead
+I: 20: Locally disabling invalid-name (C0103)
I: 21: Suppressed 'invalid-name' (from line 20)
+I: 22: Locally enabling invalid-name (C0103)
diff --git a/test/messages/func_loopvar_in_closure.txt b/test/messages/func_loopvar_in_closure.txt
index 6ca613a..5b068f4 100644
--- a/test/messages/func_loopvar_in_closure.txt
+++ b/test/messages/func_loopvar_in_closure.txt
@@ -1,8 +1,8 @@
W: 21:good_case3: Unused variable 'i'
W: 45:good_case6.<lambda>: Using possibly undefined loop variable 'i'
-W: 61:bad_case.<lambda>: Cell variable i defined in loop
-W: 66:bad_case2.<lambda>: Cell variable i defined in loop
-W: 74:bad_case3.<lambda>: Cell variable j defined in loop
-W: 84:bad_case4.nested: Cell variable i defined in loop
-W:105:bad_case5.<lambda>: Cell variable i defined in loop
-W:113:bad_case6.<lambda>: Cell variable i defined in loop
+W: 78:bad_case.<lambda>: Cell variable i defined in loop
+W: 83:bad_case2.<lambda>: Cell variable i defined in loop
+W: 91:bad_case3.<lambda>: Cell variable j defined in loop
+W:101:bad_case4.nested: Cell variable i defined in loop
+W:122:bad_case5.<lambda>: Cell variable i defined in loop
+W:130:bad_case6.<lambda>: Cell variable i defined in loop
diff --git a/test/messages/func_name_checking.txt b/test/messages/func_name_checking.txt
deleted file mode 100644
index e8d0a73..0000000
--- a/test/messages/func_name_checking.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-C: 10:Run.B: Invalid class name "B"
-C: 14:Run: Invalid variable name "bBb"
-C: 28:HOHOHOHO: Invalid function name "HOHOHOHO"
-C: 30:HOHOHOHO: Invalid variable name "HIHIHI"
-C: 33:xyz: Invalid class name "xyz"
-C: 36:xyz: Invalid class attribute name "zz"
-C: 41:xyz.Youplapoum: Invalid method name "Youplapoum"
-C: 56: Invalid constant name "benpasceluila"
-C: 62:Correct.__init__: Invalid attribute name "_Ca_va_Pas"
-C: 64:Correct.BadMethodName: Invalid method name "BadMethodName"
-C: 77: Invalid class name "BAD_NAME_FOR_CLASS"
-C: 78: Invalid class name "NEXT_BAD_NAME_FOR_CLASS"
-C: 84: Invalid class name "NOT_CORRECT"
-C: 90:test_globals: Invalid constant name "AlsoCorrect"
-C:118:FooClass.PROPERTY_NAME: Invalid attribute name "PROPERTY_NAME"
-C:123:FooClass.ABSTRACT_PROPERTY_NAME: Invalid attribute name "ABSTRACT_PROPERTY_NAME"
-C:128:FooClass.PROPERTY_NAME_SETTER: Invalid attribute name "PROPERTY_NAME_SETTER"
-C:133:func_bad_argname: Invalid argument name "NOT_GOOD"
diff --git a/test/messages/func_namedtuple.txt b/test/messages/func_namedtuple.txt
deleted file mode 100644
index d1ef2b1..0000000
--- a/test/messages/func_namedtuple.txt
+++ /dev/null
@@ -1 +0,0 @@
-E: 10: Class 'Thing' has no 'x' member
diff --git a/test/messages/func_w0404.txt b/test/messages/func_w0404.txt
index 73d770f..cd7f3e2 100644
--- a/test/messages/func_w0404.txt
+++ b/test/messages/func_w0404.txt
@@ -1,5 +1,5 @@
W: 8: Reimport 'ElementTree' (imported line 7)
-W: 11: Reimport 'multiprocessing.pool' (imported line 10)
+W: 11: Reimport 'email.encoders' (imported line 10)
W: 13: Reimport 'sys' (imported line 5)
W: 23:reimport: Redefining name 'sys' from outer scope (line 5)
W: 23:reimport: Reimport 'sys' (imported line 5)
diff --git a/test/test_func.py b/test/test_func.py
index d3fb9be..2d573c2 100644
--- a/test/test_func.py
+++ b/test/test_func.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2003-2008 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
@@ -44,12 +44,12 @@ class LintTestNonExistentModuleTC(LintTestUsingModule):
class TestTests(testlib.TestCase):
"""check that all testable messages have been checked"""
- PORTED = set(['I0001', 'I0010', 'W0712', 'E1001'])
-
+ PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310'])
+
@testlib.tag('coverage')
def test_exhaustivity(self):
# skip fatal messages
- not_tested = set(msg.msgid for msg in linter.messages
+ not_tested = set(msg.msgid for msg in linter.msgs_store.messages
if msg.msgid[0] != 'F' and msg.may_be_emitted())
for msgid in test_reporter.message_ids:
try:
diff --git a/test/test_functional.py b/test/test_functional.py
index ae12360..bd65ce3 100644
--- a/test/test_functional.py
+++ b/test/test_functional.py
@@ -1,4 +1,5 @@
"""Functional full-module tests for PyLint."""
+from __future__ import with_statement
import ConfigParser
import cStringIO
import operator
@@ -21,7 +22,7 @@ UPDATE = False
# Common sub-expressions.
_MESSAGE = {'msg': r'[a-z][a-z\-]+'}
-# Matches a #,
+# Matches a #,
# - followed by a comparison operator and a Python version (optional),
# - followed by an line number with a +/- (optional),
# - followed by a list of bracketed message symbols.
@@ -39,7 +40,7 @@ def parse_python_version(str):
class TestReporter(reporters.BaseReporter):
def add_message(self, msg_id, location, msg):
self.messages.append(reporters.Message(self, msg_id, location, msg))
-
+
def on_set_current_module(self, module, filepath):
self.messages = []
@@ -88,15 +89,15 @@ class TestFile(object):
@property
def expected_output(self):
- return self._file_type('.txt')
+ return self._file_type('.txt', check_exists=False)
@property
def source(self):
return self._file_type('.py')
- def _file_type(self, ext):
+ def _file_type(self, ext, check_exists=True):
name = os.path.join(self._directory, self.base + ext)
- if os.path.exists(name):
+ if not check_exists or os.path.exists(name):
return name
else:
raise NoFileError
@@ -171,12 +172,12 @@ class LintModuleTest(testlib.TestCase):
self._linter = lint.PyLinter()
self._linter.set_reporter(test_reporter)
self._linter.config.persistent = 0
+ checkers.initialize(self._linter)
self._linter.disable('I')
try:
self._linter.load_file_configuration(test_file.option_file)
except NoFileError:
pass
- checkers.initialize(self._linter)
self._test_file = test_file
def shortDescription(self):
@@ -188,7 +189,7 @@ class LintModuleTest(testlib.TestCase):
def _get_expected(self):
with open(self._test_file.source) as fobj:
expected = get_expected_messages(fobj)
-
+
lines = []
if self._produces_output() and expected:
with open(self._test_file.expected_output, 'U') as fobj:
@@ -200,7 +201,7 @@ class LintModuleTest(testlib.TestCase):
return expected, ''.join(lines)
def _get_received(self):
- messages = self._linter.reporter.messages
+ messages = self._linter.reporter.messages
messages.sort(key=lambda m: (m.line, m.C, m.msg))
text_result = cStringIO.StringIO()
received = {}
@@ -219,7 +220,7 @@ class LintModuleTest(testlib.TestCase):
if expected_messages != received_messages:
msg = ['Wrong results for file "%s":' % (self._test_file.base)]
- missing, unexpected = multiset_difference(expected_messages,
+ missing, unexpected = multiset_difference(expected_messages,
received_messages)
if missing:
msg.append('\nExpected in testdata:')
@@ -249,7 +250,7 @@ def active_in_running_python_version(options):
def suite():
- input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ input_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'functional')
suite = testlib.TestSuite()
for fname in os.listdir(input_dir):
diff --git a/test/test_regr.py b/test/test_regr.py
index 0349481..8bc50c2 100644
--- a/test/test_regr.py
+++ b/test/test_regr.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2005 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2005-2014 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
@@ -60,7 +60,7 @@ class NonRegrTC(TestCase):
got = linter.reporter.finalize().strip()
checked = linter.stats['by_module'].keys()
self.assertEqual(checked, ['package.__init__'],
- '%s: %s' % (variation, checked))
+ '%s: %s' % (variation, checked))
cwd = os.getcwd()
os.chdir(join(REGR_DATA, 'package'))
sys.path.insert(0, '')
diff --git a/test/unittest_checker_base.py b/test/unittest_checker_base.py
index 972c783..1ac14b9 100644
--- a/test/unittest_checker_base.py
+++ b/test/unittest_checker_base.py
@@ -1,6 +1,8 @@
"""Unittest for the base checker."""
+from __future__ import with_statement
import re
+import sys
from astroid import test_utils
from pylint.checkers import base
@@ -80,6 +82,8 @@ class NameCheckerTest(CheckerTestCase):
@set_config(attr_rgx=re.compile('[A-Z]+'))
def test_property_names(self):
+ if sys.version_info < (2, 6):
+ self.skip('abc module does not exist on 2.5')
# If a method is annotated with @property, it's name should
# match the attr regex. Since by default the attribute regex is the same
# as the method regex, we override it here.
diff --git a/test/unittest_checker_classes.py b/test/unittest_checker_classes.py
index 388a24a..b185322 100644
--- a/test/unittest_checker_classes.py
+++ b/test/unittest_checker_classes.py
@@ -1,4 +1,5 @@
"""Unit tests for the variables checker."""
+from __future__ import with_statement
from astroid import test_utils
from pylint.checkers import classes
diff --git a/test/unittest_checker_format.py b/test/unittest_checker_format.py
index b9fba5c..82b079a 100644
--- a/test/unittest_checker_format.py
+++ b/test/unittest_checker_format.py
@@ -15,6 +15,7 @@
Check format checker helper functions
"""
+from __future__ import with_statement
import sys
import re
diff --git a/test/unittest_checker_logging.py b/test/unittest_checker_logging.py
index cbacada..c36aec9 100644
--- a/test/unittest_checker_logging.py
+++ b/test/unittest_checker_logging.py
@@ -2,6 +2,7 @@
"""
Unittest for the logging checker.
"""
+from __future__ import with_statement
from logilab.common.testlib import unittest_main
from astroid import test_utils
diff --git a/test/unittest_checker_misc.py b/test/unittest_checker_misc.py
index 670d53c..a494b1a 100644
--- a/test/unittest_checker_misc.py
+++ b/test/unittest_checker_misc.py
@@ -15,6 +15,8 @@
"""
Tests for the misc checker.
"""
+from __future__ import with_statement
+
import sys
import tempfile
import os
diff --git a/test/unittest_checker_typecheck.py b/test/unittest_checker_typecheck.py
index 0a8687b..93c73ea 100644
--- a/test/unittest_checker_typecheck.py
+++ b/test/unittest_checker_typecheck.py
@@ -1,4 +1,5 @@
"""Unittest for the type checker."""
+from __future__ import with_statement
from astroid import test_utils
from pylint.checkers import typecheck
diff --git a/test/unittest_checker_variables.py b/test/unittest_checker_variables.py
index 20a0d9e..a3776d7 100644
--- a/test/unittest_checker_variables.py
+++ b/test/unittest_checker_variables.py
@@ -1,4 +1,5 @@
"""Unit tests for the variables checker."""
+from __future__ import with_statement
import sys
import os
diff --git a/test/unittest_checkers_utils.py b/test/unittest_checkers_utils.py
index 72108d7..40186f5 100644
--- a/test/unittest_checkers_utils.py
+++ b/test/unittest_checkers_utils.py
@@ -15,6 +15,7 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""test the pylint.checkers.utils module
"""
+from __future__ import with_statement
__revision__ = '$Id: unittest_checkers_utils.py,v 1.6 2005-11-02 09:22:07 syt Exp $'
diff --git a/test/unittest_lint.py b/test/unittest_lint.py
index 7dd403b..d9185f6 100644
--- a/test/unittest_lint.py
+++ b/test/unittest_lint.py
@@ -27,15 +27,16 @@ from pylint import config
from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \
ArgumentPreprocessingError
from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \
- PyLintASTWalker, MessageDefinition, build_message_def, tokenize_module
+ MessagesStore, PyLintASTWalker, MessageDefinition, FileState, \
+ build_message_def, tokenize_module
from pylint.testutils import TestReporter
from pylint.reporters import text
from pylint import checkers
if sys.platform == 'win32':
- HOME = 'USERPROFILE'
+ HOME = 'USERPROFILE'
else:
- HOME = 'HOME'
+ HOME = 'HOME'
class GetNoteMessageTC(TestCase):
def test(self):
@@ -60,55 +61,15 @@ class PyLinterTC(TestCase):
checkers.initialize(self.linter)
self.linter.set_reporter(TestReporter())
- def _compare_messages(self, desc, msg, checkerref=False):
- # replace \r\n with \n, because
- # logilab.common.textutils.normalize_text
- # uses os.linesep, which will
- # not properly compare with triple
- # quoted multilines used in these tests
- self.assertMultiLineEqual(desc,
- msg.format_help(checkerref=checkerref)
- .replace('\r\n', '\n'))
-
- def test_check_message_id(self):
- self.assertIsInstance(self.linter.check_message_id('F0001'),
- MessageDefinition)
- self.assertRaises(UnknownMessage,
- self.linter.check_message_id, 'YB12')
-
- def test_message_help(self):
- msg = self.linter.check_message_id('F0001')
- self._compare_messages(
- ''':fatal (F0001):
- Used when an error occurred preventing the analysis of a module (unable to
- find it for instance). This message belongs to the master checker.''',
- msg, checkerref=True)
- self._compare_messages(
- ''':fatal (F0001):
- Used when an error occurred preventing the analysis of a module (unable to
- find it for instance).''',
- msg, checkerref=False)
-
- def test_message_help_minmax(self):
- # build the message manually to be python version independant
- msg = build_message_def(self.linter._checkers['typecheck'][0],
- 'E1122', checkers.typecheck.MSGS['E1122'])
- self._compare_messages(
- ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in %s call*
- Used when a function call passes the same keyword argument multiple times.
- This message belongs to the typecheck checker. It can't be emitted when using
- Python >= 2.6.''',
- msg, checkerref=True)
- self._compare_messages(
- ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in %s call*
- Used when a function call passes the same keyword argument multiple times.
- This message can't be emitted when using Python >= 2.6.''',
- msg, checkerref=False)
-
- def test_enable_message(self):
+ def init_linter(self):
linter = self.linter
linter.open()
linter.set_current_module('toto')
+ linter.file_state = FileState('toto')
+ return linter
+
+ def test_enable_message(self):
+ linter = self.init_linter()
self.assertTrue(linter.is_message_enabled('W0101'))
self.assertTrue(linter.is_message_enabled('W0102'))
linter.disable('W0101', scope='package')
@@ -124,9 +85,7 @@ class PyLinterTC(TestCase):
self.assertTrue(linter.is_message_enabled('W0102', 1))
def test_enable_message_category(self):
- linter = self.linter
- linter.open()
- linter.set_current_module('toto')
+ linter = self.init_linter()
self.assertTrue(linter.is_message_enabled('W0101'))
self.assertTrue(linter.is_message_enabled('C0121'))
linter.disable('W', scope='package')
@@ -144,31 +103,29 @@ class PyLinterTC(TestCase):
self.assertTrue(linter.is_message_enabled('C0121', line=1))
def test_message_state_scope(self):
- linter = self.linter
- linter.open()
+ linter = self.init_linter()
+ fs = linter.file_state
linter.disable('C0121')
self.assertEqual(MSG_STATE_SCOPE_CONFIG,
- linter.get_message_state_scope('C0121'))
+ fs._message_state_scope('C0121'))
linter.disable('W0101', scope='module', line=3)
self.assertEqual(MSG_STATE_SCOPE_CONFIG,
- linter.get_message_state_scope('C0121'))
+ fs._message_state_scope('C0121'))
self.assertEqual(MSG_STATE_SCOPE_MODULE,
- linter.get_message_state_scope('W0101', 3))
+ fs._message_state_scope('W0101', 3))
linter.enable('W0102', scope='module', line=3)
self.assertEqual(MSG_STATE_SCOPE_MODULE,
- linter.get_message_state_scope('W0102', 3))
+ fs._message_state_scope('W0102', 3))
def test_enable_message_block(self):
- linter = self.linter
+ linter = self.init_linter()
linter.open()
filepath = join(INPUTDIR, 'func_block_disable_msg.py')
linter.set_current_module('func_block_disable_msg')
astroid = linter.get_ast(filepath, 'func_block_disable_msg')
linter.process_tokens(tokenize_module(astroid))
- orig_state = linter._module_msgs_state.copy()
- linter._module_msgs_state = {}
- linter._suppression_mapping = {}
- linter.collect_block_lines(astroid, orig_state)
+ fs = linter.file_state
+ fs.collect_block_lines(linter.msgs_store, astroid)
# global (module level)
self.assertTrue(linter.is_message_enabled('W0613'))
self.assertTrue(linter.is_message_enabled('E1101'))
@@ -205,25 +162,24 @@ class PyLinterTC(TestCase):
self.assertTrue(linter.is_message_enabled('E1101', 75))
self.assertTrue(linter.is_message_enabled('E1101', 77))
- self.assertEqual(17, linter._suppression_mapping['W0613', 18])
- self.assertEqual(30, linter._suppression_mapping['E1101', 33])
- self.assertTrue(('E1101', 46) not in linter._suppression_mapping)
- self.assertEqual(1, linter._suppression_mapping['C0302', 18])
- self.assertEqual(1, linter._suppression_mapping['C0302', 50])
+ fs = linter.file_state
+ self.assertEqual(17, fs._suppression_mapping['W0613', 18])
+ self.assertEqual(30, fs._suppression_mapping['E1101', 33])
+ self.assertTrue(('E1101', 46) not in fs._suppression_mapping)
+ self.assertEqual(1, fs._suppression_mapping['C0302', 18])
+ self.assertEqual(1, fs._suppression_mapping['C0302', 50])
# This is tricky. While the disable in line 106 is disabling
# both 108 and 110, this is usually not what the user wanted.
# Therefore, we report the closest previous disable comment.
- self.assertEqual(106, linter._suppression_mapping['E1101', 108])
- self.assertEqual(109, linter._suppression_mapping['E1101', 110])
+ self.assertEqual(106, fs._suppression_mapping['E1101', 108])
+ self.assertEqual(109, fs._suppression_mapping['E1101', 110])
def test_enable_by_symbol(self):
"""messages can be controlled by symbolic names.
The state is consistent across symbols and numbers.
"""
- linter = self.linter
- linter.open()
- linter.set_current_module('toto')
+ linter = self.init_linter()
self.assertTrue(linter.is_message_enabled('W0101'))
self.assertTrue(linter.is_message_enabled('unreachable'))
self.assertTrue(linter.is_message_enabled('W0102'))
@@ -246,16 +202,6 @@ class PyLinterTC(TestCase):
self.assertTrue(linter.is_message_enabled('W0102', 1))
self.assertTrue(linter.is_message_enabled('dangerous-default-value', 1))
- def test_list_messages(self):
- sys.stdout = StringIO()
- try:
- self.linter.list_messages()
- output = sys.stdout.getvalue()
- finally:
- sys.stdout = sys.__stdout__
- # cursory examination of the output: we're mostly testing it completes
- self.assertIn(':empty-docstring (C0112): *Empty %s docstring*', output)
-
def test_lint_ext_module_with_file_output(self):
self.linter.set_reporter(text.TextReporter())
if sys.version_info < (3, 0):
@@ -363,28 +309,11 @@ class PyLinterTC(TestCase):
['C: 1: Line too long (1/2)', 'C: 2: Line too long (3/4)'],
self.linter.reporter.messages)
- def test_add_renamed_message(self):
- self.linter.add_renamed_message('C9999', 'old-bad-name', 'invalid-name')
- self.assertEqual('invalid-name',
- self.linter.check_message_id('C9999').symbol)
- self.assertEqual('invalid-name',
- self.linter.check_message_id('old-bad-name').symbol)
-
- def test_renamed_message_register(self):
- class Checker(object):
- msgs = {'W1234': ('message', 'msg-symbol', 'msg-description',
- {'old_names': [('W0001', 'old-symbol')]})}
- self.linter.register_messages(Checker())
- self.assertEqual('msg-symbol',
- self.linter.check_message_id('W0001').symbol)
- self.assertEqual('msg-symbol',
- self.linter.check_message_id('old-symbol').symbol)
-
def test_init_hooks_called_before_load_plugins(self):
- self.assertRaises(RuntimeError,
- Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError'])
- self.assertRaises(RuntimeError,
- Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant'])
+ self.assertRaises(RuntimeError,
+ Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError'])
+ self.assertRaises(RuntimeError,
+ Run, ['--init-hook', 'raise RuntimeError', '--load-plugins', 'unexistant'])
def test_analyze_explicit_script(self):
@@ -394,6 +323,7 @@ class PyLinterTC(TestCase):
['C: 2: Line too long (175/80)'],
self.linter.reporter.messages)
+
class ConfigTC(TestCase):
def setUp(self):
@@ -540,5 +470,86 @@ class PreprocessOptionsTC(TestCase):
{'bar' : (None, False)})
+class MessagesStoreTC(TestCase):
+ def setUp(self):
+ self.store = MessagesStore()
+ class Checker(object):
+ name = 'achecker'
+ msgs = {
+ 'W1234': ('message', 'msg-symbol', 'msg description.',
+ {'old_names': [('W0001', 'old-symbol')]}),
+ 'E1234': ('Duplicate keyword argument %r in %s call',
+ 'duplicate-keyword-arg',
+ 'Used when a function call passes the same keyword argument multiple times.',
+ {'maxversion': (2, 6)}),
+ }
+ self.store.register_messages(Checker())
+
+ def _compare_messages(self, desc, msg, checkerref=False):
+ # replace \r\n with \n, because
+ # logilab.common.textutils.normalize_text
+ # uses os.linesep, which will
+ # not properly compare with triple
+ # quoted multilines used in these tests
+ self.assertMultiLineEqual(
+ desc,
+ msg.format_help(checkerref=checkerref).replace('\r\n', '\n'))
+
+ def test_check_message_id(self):
+ self.assertIsInstance(self.store.check_message_id('W1234'),
+ MessageDefinition)
+ self.assertRaises(UnknownMessage,
+ self.store.check_message_id, 'YB12')
+
+ def test_message_help(self):
+ msg = self.store.check_message_id('W1234')
+ self._compare_messages(
+ ''':msg-symbol (W1234): *message*
+ msg description. This message belongs to the achecker checker.''',
+ msg, checkerref=True)
+ self._compare_messages(
+ ''':msg-symbol (W1234): *message*
+ msg description.''',
+ msg, checkerref=False)
+
+ def test_message_help_minmax(self):
+ # build the message manually to be python version independant
+ msg = self.store.check_message_id('E1234')
+ self._compare_messages(
+ ''':duplicate-keyword-arg (E1234): *Duplicate keyword argument %r in %s call*
+ Used when a function call passes the same keyword argument multiple times.
+ This message belongs to the achecker checker. It can't be emitted when using
+ Python >= 2.6.''',
+ msg, checkerref=True)
+ self._compare_messages(
+ ''':duplicate-keyword-arg (E1234): *Duplicate keyword argument %r in %s call*
+ Used when a function call passes the same keyword argument multiple times.
+ This message can't be emitted when using Python >= 2.6.''',
+ msg, checkerref=False)
+
+ def test_list_messages(self):
+ sys.stdout = StringIO()
+ try:
+ self.store.list_messages()
+ output = sys.stdout.getvalue()
+ finally:
+ sys.stdout = sys.__stdout__
+ # cursory examination of the output: we're mostly testing it completes
+ self.assertIn(':msg-symbol (W1234): *message*', output)
+
+ def test_add_renamed_message(self):
+ self.store.add_renamed_message('W1234', 'old-bad-name', 'msg-symbol')
+ self.assertEqual('msg-symbol',
+ self.store.check_message_id('W1234').symbol)
+ self.assertEqual('msg-symbol',
+ self.store.check_message_id('old-bad-name').symbol)
+
+ def test_renamed_message_register(self):
+ self.assertEqual('msg-symbol',
+ self.store.check_message_id('W0001').symbol)
+ self.assertEqual('msg-symbol',
+ self.store.check_message_id('old-symbol').symbol)
+
+
if __name__ == '__main__':
unittest_main()
diff --git a/test/unittest_reporting.py b/test/unittest_reporting.py
index 3dd0d0a..e1cf161 100644
--- a/test/unittest_reporting.py
+++ b/test/unittest_reporting.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
diff --git a/testutils.py b/testutils.py
index daf7477..ef423c5 100644
--- a/testutils.py
+++ b/testutils.py
@@ -122,10 +122,30 @@ class TestReporter(BaseReporter):
"""ignore layouts"""
-class Message(collections.namedtuple('Message',
- ['msg_id', 'line', 'node', 'args'])):
- def __new__(cls, msg_id, line=None, node=None, args=None):
- return tuple.__new__(cls, (msg_id, line, node, args))
+if sys.version_info < (2, 6):
+ class Message(tuple):
+ def __new__(cls, msg_id, line=None, node=None, args=None):
+ return tuple.__new__(cls, (msg_id, line, node, args))
+
+ @property
+ def msg_id(self):
+ return self[0]
+ @property
+ def line(self):
+ return self[1]
+ @property
+ def node(self):
+ return self[2]
+ @property
+ def args(self):
+ return self[3]
+
+
+else:
+ class Message(collections.namedtuple('Message',
+ ['msg_id', 'line', 'node', 'args'])):
+ def __new__(cls, msg_id, line=None, node=None, args=None):
+ return tuple.__new__(cls, (msg_id, line, node, args))
class UnittestLinter(object):
diff --git a/utils.py b/utils.py
index 6fbb480..65e9ce0 100644
--- a/utils.py
+++ b/utils.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE).
+# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This program is free software; you can redistribute it and/or modify it under
@@ -190,61 +190,8 @@ class MessagesHandlerMixIn(object):
"""
def __init__(self):
- # Primary registry for all active messages (i.e. all messages
- # that can be emitted by pylint for the underlying Python
- # version). It contains the 1:1 mapping from symbolic names
- # to message definition objects.
- self._messages = {}
- # Maps alternative names (numeric IDs, deprecated names) to
- # message definitions. May contain several names for each definition
- # object.
- self._alternative_names = {}
self._msgs_state = {}
- self._module_msgs_state = {} # None
- self._raw_module_msgs_state = {}
- self._msgs_by_category = {}
self.msg_status = 0
- self._ignored_msgs = {}
- self._suppression_mapping = {}
-
- def add_renamed_message(self, old_id, old_symbol, new_symbol):
- """Register the old ID and symbol for a warning that was renamed.
-
- This allows users to keep using the old ID/symbol in suppressions.
- """
- msg = self.check_message_id(new_symbol)
- msg.old_names.append((old_id, old_symbol))
- self._alternative_names[old_id] = msg
- self._alternative_names[old_symbol] = msg
-
- def register_messages(self, checker):
- """register a dictionary of messages
-
- Keys are message ids, values are a 2-uple with the message type and the
- message itself
-
- message ids should be a string of len 4, where the two first characters
- are the checker id and the two last the message id in this checker
- """
- chkid = None
- for msgid, msg_tuple in checker.msgs.iteritems():
- msg = build_message_def(checker, msgid, msg_tuple)
- assert msg.symbol not in self._messages, \
- 'Message symbol %r is already defined' % msg.symbol
- # avoid duplicate / malformed ids
- assert msg.msgid not in self._alternative_names, \
- 'Message id %r is already defined' % msgid
- assert chkid is None or chkid == msg.msgid[1:3], \
- 'Inconsistent checker part in message id %r' % msgid
- chkid = msg.msgid[1:3]
- if not msg.may_be_emitted():
- self._msgs_state[msg.msgid] = False
- self._messages[msg.symbol] = msg
- self._alternative_names[msg.msgid] = msg
- for old_id, old_symbol in msg.old_names:
- self._alternative_names[old_id] = msg
- self._alternative_names[old_symbol] = msg
- self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid)
def disable(self, msgid, scope='package', line=None, ignore_unknown=False):
"""don't output message of the given id"""
@@ -257,14 +204,15 @@ class MessagesHandlerMixIn(object):
# msgid is a category?
catid = category_id(msgid)
if catid is not None:
- for _msgid in self._msgs_by_category.get(catid):
+ for _msgid in self.msgs_store._msgs_by_category.get(catid):
self.disable(_msgid, scope, line)
return
# msgid is a checker name?
if msgid.lower() in self._checkers:
+ msgs_store = self.msgs_store
for checker in self._checkers[msgid.lower()]:
for _msgid in checker.msgs:
- if _msgid in self._alternative_names:
+ if _msgid in msgs_store._alternative_names:
self.disable(_msgid, scope, line)
return
# msgid is report id?
@@ -274,21 +222,17 @@ class MessagesHandlerMixIn(object):
try:
# msgid is a symbolic or numeric msgid.
- msg = self.check_message_id(msgid)
+ msg = self.msgs_store.check_message_id(msgid)
except UnknownMessage:
if ignore_unknown:
return
raise
if scope == 'module':
- assert line > 0
- try:
- self._module_msgs_state[msg.msgid][line] = False
- except KeyError:
- self._module_msgs_state[msg.msgid] = {line: False}
- if msg.symbol != 'locally-disabled':
- self.add_message('locally-disabled', line=line,
- args=(msg.symbol, msg.msgid))
+ self.file_state.set_msg_status(msg, line, False)
+ if msg.symbol != 'locally-disabled':
+ self.add_message('locally-disabled', line=line,
+ args=(msg.symbol, msg.msgid))
else:
msgs = self._msgs_state
@@ -303,7 +247,7 @@ class MessagesHandlerMixIn(object):
catid = category_id(msgid)
# msgid is a category?
if catid is not None:
- for msgid in self._msgs_by_category.get(catid):
+ for msgid in self.msgs_store._msgs_by_category.get(catid):
self.enable(msgid, scope, line)
return
# msgid is a checker name?
@@ -319,56 +263,21 @@ class MessagesHandlerMixIn(object):
try:
# msgid is a symbolic or numeric msgid.
- msg = self.check_message_id(msgid)
+ msg = self.msgs_store.check_message_id(msgid)
except UnknownMessage:
if ignore_unknown:
return
raise
if scope == 'module':
- assert line > 0
- try:
- self._module_msgs_state[msg.msgid][line] = True
- except KeyError:
- self._module_msgs_state[msg.msgid] = {line: True}
- self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid))
+ self.file_state.set_msg_status(msg, line, True)
+ self.add_message('locally-enabled', line=line, args=(msg.symbol, msg.msgid))
else:
msgs = self._msgs_state
msgs[msg.msgid] = True
# sync configuration object
self.config.enable = [mid for mid, val in msgs.iteritems() if val]
- def check_message_id(self, msgid):
- """returns the Message object for this message.
-
- msgid may be either a numeric or symbolic id.
-
- Raises UnknownMessage if the message id is not defined.
- """
- if msgid[1:].isdigit():
- msgid = msgid.upper()
- for source in (self._alternative_names, self._messages):
- try:
- return source[msgid]
- except KeyError:
- pass
- raise UnknownMessage('No such message id %s' % msgid)
-
- def get_msg_display_string(self, msgid):
- """Generates a user-consumable representation of a message.
-
- Can be just the message ID or the ID and the symbol.
- """
- return repr(self.check_message_id(msgid).symbol)
-
- def get_message_state_scope(self, msgid, line=None):
- """Returns the scope at which a message was enabled/disabled."""
- try:
- if line in self._module_msgs_state[msgid]:
- return MSG_STATE_SCOPE_MODULE
- except (KeyError, TypeError):
- return MSG_STATE_SCOPE_CONFIG
-
def is_message_enabled(self, msg_descr, line=None):
"""return true if the message associated to the given message id is
enabled
@@ -376,7 +285,7 @@ class MessagesHandlerMixIn(object):
msgid may be either a numeric or symbolic message id.
"""
try:
- msgid = self.check_message_id(msg_descr).msgid
+ msgid = self.msgs_store.check_message_id(msg_descr).msgid
except UnknownMessage:
# The linter checks for messages that are not registered
# due to version mismatch, just treat them as message IDs
@@ -385,24 +294,10 @@ class MessagesHandlerMixIn(object):
if line is None:
return self._msgs_state.get(msgid, True)
try:
- return self._module_msgs_state[msgid][line]
- except (KeyError, TypeError):
+ return self.file_state._module_msgs_state[msgid][line]
+ except KeyError:
return self._msgs_state.get(msgid, True)
- def handle_ignored_message(self, state_scope, msgid, line, node, args):
- """Report an ignored message.
-
- state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
- depending on whether the message was disabled locally in the module,
- or globally. The other arguments are the same as for add_message.
- """
- if state_scope == MSG_STATE_SCOPE_MODULE:
- try:
- orig_line = self._suppression_mapping[(msgid, line)]
- self._ignored_msgs.setdefault((msgid, orig_line), set()).add(line)
- except KeyError:
- pass
-
def add_message(self, msg_descr, line=None, node=None, args=None):
"""Adds a message given by ID or name.
@@ -412,7 +307,7 @@ class MessagesHandlerMixIn(object):
provide line if the line number is different), raw and token checkers
must provide the line argument.
"""
- msg_info = self.check_message_id(msg_descr)
+ msg_info = self.msgs_store.check_message_id(msg_descr)
msgid = msg_info.msgid
# backward compatibility, message may not have a symbol
symbol = msg_info.symbol or msgid
@@ -434,8 +329,7 @@ class MessagesHandlerMixIn(object):
col_offset = None
# should this message be displayed
if not self.is_message_enabled(msgid, line):
- self.handle_ignored_message(
- self.get_message_state_scope(msgid, line), msgid, line, node, args)
+ self.file_state.handle_ignored_message(msgid, line, node, args)
return
# update stats
msg_cat = MSG_TYPES[msgid[0]]
@@ -460,17 +354,6 @@ class MessagesHandlerMixIn(object):
# add the message
self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offset or 0), msg)
- def help_message(self, msgids):
- """display help messages for the given message identifiers"""
- for msgid in msgids:
- try:
- print self.check_message_id(msgid).format_help(checkerref=True)
- print
- except UnknownMessage, ex:
- print ex
- print
- continue
-
def print_full_documentation(self):
"""output a full documentation in ReST format"""
by_checker = {}
@@ -528,11 +411,217 @@ class MessagesHandlerMixIn(object):
print
print
+
+class FileState(object):
+ """Hold internal state specific to the currently analyzed file"""
+
+ def __init__(self, modname=None):
+ self.base_name = modname
+ self._module_msgs_state = {}
+ self._raw_module_msgs_state = {}
+ self._ignored_msgs = {}
+ self._suppression_mapping = {}
+
+ def collect_block_lines(self, msgs_store, module_node):
+ """Walk the AST to collect block level options line numbers."""
+ for msg, lines in self._module_msgs_state.iteritems():
+ self._raw_module_msgs_state[msg] = lines.copy()
+ orig_state = self._module_msgs_state.copy()
+ self._module_msgs_state = {}
+ self._suppression_mapping = {}
+ self._collect_block_lines(msgs_store, module_node, orig_state)
+
+ def _collect_block_lines(self, msgs_store, node, msg_state):
+ """Recursivly walk (depth first) AST to collect block level options line
+ numbers.
+ """
+ for child in node.get_children():
+ self._collect_block_lines(msgs_store, child, msg_state)
+ first = node.fromlineno
+ last = node.tolineno
+ # first child line number used to distinguish between disable
+ # which are the first child of scoped node with those defined later.
+ # For instance in the code below:
+ #
+ # 1. def meth8(self):
+ # 2. """test late disabling"""
+ # 3. # pylint: disable=E1102
+ # 4. print self.blip
+ # 5. # pylint: disable=E1101
+ # 6. print self.bla
+ #
+ # E1102 should be disabled from line 1 to 6 while E1101 from line 5 to 6
+ #
+ # this is necessary to disable locally messages applying to class /
+ # function using their fromlineno
+ if isinstance(node, (nodes.Module, nodes.Class, nodes.Function)) and node.body:
+ firstchildlineno = node.body[0].fromlineno
+ else:
+ firstchildlineno = last
+ for msgid, lines in msg_state.iteritems():
+ for lineno, state in lines.items():
+ original_lineno = lineno
+ if first <= lineno <= last:
+ # Set state for all lines for this block, if the
+ # warning is applied to nodes.
+ if msgs_store.check_message_id(msgid).scope == WarningScope.NODE:
+ if lineno > firstchildlineno:
+ state = True
+ first_, last_ = node.block_range(lineno)
+ else:
+ first_ = lineno
+ last_ = last
+ for line in xrange(first_, last_+1):
+ # do not override existing entries
+ if not line in self._module_msgs_state.get(msgid, ()):
+ if line in lines: # state change in the same block
+ state = lines[line]
+ original_lineno = line
+ if not state:
+ self._suppression_mapping[(msgid, line)] = original_lineno
+ try:
+ self._module_msgs_state[msgid][line] = state
+ except KeyError:
+ self._module_msgs_state[msgid] = {line: state}
+ del lines[lineno]
+
+ def set_msg_status(self, msg, line, status):
+ """Set status (enabled/disable) for a given message at a given line"""
+ assert line > 0
+ try:
+ self._module_msgs_state[msg.msgid][line] = status
+ except KeyError:
+ self._module_msgs_state[msg.msgid] = {line: status}
+
+ def handle_ignored_message(self, msgid, line, node, args):
+ """Report an ignored message.
+
+ state_scope is either MSG_STATE_SCOPE_MODULE or MSG_STATE_SCOPE_CONFIG,
+ depending on whether the message was disabled locally in the module,
+ or globally. The other arguments are the same as for add_message.
+ """
+ state_scope = self._message_state_scope(msgid, line)
+ if state_scope == MSG_STATE_SCOPE_MODULE:
+ try:
+ orig_line = self._suppression_mapping[(msgid, line)]
+ self._ignored_msgs.setdefault((msgid, orig_line), set()).add(line)
+ except KeyError:
+ pass
+
+ def _message_state_scope(self, msgid, line=None):
+ """Returns the scope at which a message was enabled/disabled."""
+ try:
+ if line in self._module_msgs_state[msgid]:
+ return MSG_STATE_SCOPE_MODULE
+ except KeyError:
+ return MSG_STATE_SCOPE_CONFIG
+
+ def iter_spurious_suppression_messages(self, msgs_store):
+ for warning, lines in self._raw_module_msgs_state.iteritems():
+ for line, enable in lines.iteritems():
+ if not enable and (warning, line) not in self._ignored_msgs:
+ yield 'useless-suppression', line, \
+ (msgs_store.get_msg_display_string(warning),)
+ # don't use iteritems here, _ignored_msgs may be modified by add_message
+ for (warning, from_), lines in self._ignored_msgs.items():
+ for line in lines:
+ yield 'suppressed-message', line, \
+ (msgs_store.get_msg_display_string(warning), from_)
+
+
+class MessagesStore(object):
+ """The messages store knows information about every possible message but has
+ no particular state during analysis.
+ """
+
+ def __init__(self):
+ # Primary registry for all active messages (i.e. all messages
+ # that can be emitted by pylint for the underlying Python
+ # version). It contains the 1:1 mapping from symbolic names
+ # to message definition objects.
+ self._messages = {}
+ # Maps alternative names (numeric IDs, deprecated names) to
+ # message definitions. May contain several names for each definition
+ # object.
+ self._alternative_names = {}
+ self._msgs_by_category = {}
+
@property
def messages(self):
"""The list of all active messages."""
return self._messages.itervalues()
+ def add_renamed_message(self, old_id, old_symbol, new_symbol):
+ """Register the old ID and symbol for a warning that was renamed.
+
+ This allows users to keep using the old ID/symbol in suppressions.
+ """
+ msg = self.check_message_id(new_symbol)
+ msg.old_names.append((old_id, old_symbol))
+ self._alternative_names[old_id] = msg
+ self._alternative_names[old_symbol] = msg
+
+ def register_messages(self, checker):
+ """register a dictionary of messages
+
+ Keys are message ids, values are a 2-uple with the message type and the
+ message itself
+
+ message ids should be a string of len 4, where the two first characters
+ are the checker id and the two last the message id in this checker
+ """
+ chkid = None
+ for msgid, msg_tuple in checker.msgs.iteritems():
+ msg = build_message_def(checker, msgid, msg_tuple)
+ assert msg.symbol not in self._messages, \
+ 'Message symbol %r is already defined' % msg.symbol
+ # avoid duplicate / malformed ids
+ assert msg.msgid not in self._alternative_names, \
+ 'Message id %r is already defined' % msgid
+ assert chkid is None or chkid == msg.msgid[1:3], \
+ 'Inconsistent checker part in message id %r' % msgid
+ chkid = msg.msgid[1:3]
+ self._messages[msg.symbol] = msg
+ self._alternative_names[msg.msgid] = msg
+ for old_id, old_symbol in msg.old_names:
+ self._alternative_names[old_id] = msg
+ self._alternative_names[old_symbol] = msg
+ self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid)
+
+ def check_message_id(self, msgid):
+ """returns the Message object for this message.
+
+ msgid may be either a numeric or symbolic id.
+
+ Raises UnknownMessage if the message id is not defined.
+ """
+ if msgid[1:].isdigit():
+ msgid = msgid.upper()
+ for source in (self._alternative_names, self._messages):
+ try:
+ return source[msgid]
+ except KeyError:
+ pass
+ raise UnknownMessage('No such message id %s' % msgid)
+
+ def get_msg_display_string(self, msgid):
+ """Generates a user-consumable representation of a message.
+
+ Can be just the message ID or the ID and the symbol.
+ """
+ return repr(self.check_message_id(msgid).symbol)
+
+ def help_message(self, msgids):
+ """display help messages for the given message identifiers"""
+ for msgid in msgids:
+ try:
+ print self.check_message_id(msgid).format_help(checkerref=True)
+ print
+ except UnknownMessage, ex:
+ print ex
+ print
+ continue
+
def list_messages(self):
"""output full messages list documentation in ReST format"""
msgs = sorted(self._messages.itervalues(), key=lambda msg: msg.msgid)