diff options
author | Michal Nowikowski <godfryd@gmail.com> | 2014-07-25 13:37:25 +0200 |
---|---|---|
committer | Michal Nowikowski <godfryd@gmail.com> | 2014-07-25 13:37:25 +0200 |
commit | af2dbfc0d0952dfe4f039627101d5d60fb415882 (patch) | |
tree | 20b19b2df4723b05a82392269cf211620117cccc | |
parent | de0f498b20df4cf9cb540106a5dd98117fb61fd0 (diff) | |
parent | ee26e69c938d62c3fc48cd6848bee35ccce70637 (diff) | |
download | pylint-af2dbfc0d0952dfe4f039627101d5d60fb415882.tar.gz |
Merged logilab/pylint into default
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): @@ -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): @@ -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) |