diff options
author | Sylvain Th?nault <sylvain.thenault@logilab.fr> | 2014-07-24 18:21:12 +0200 |
---|---|---|
committer | Sylvain Th?nault <sylvain.thenault@logilab.fr> | 2014-07-24 18:21:12 +0200 |
commit | 109090dfa67f1a7a35db5002346795bb717bf7d0 (patch) | |
tree | 9c0537f58ba447b8f3bd1004f3f17c6ceb338b71 | |
parent | 640161196f431821182e4cfac5dd746a7ae464e4 (diff) | |
download | pylint-109090dfa67f1a7a35db5002346795bb717bf7d0.tar.gz |
extract a messages store from the MessagesHandlerMixIn class
-rw-r--r-- | lint.py | 25 | ||||
-rw-r--r-- | reporters/__init__.py | 2 | ||||
-rw-r--r-- | test/test_func.py | 6 | ||||
-rw-r--r-- | test/test_regr.py | 4 | ||||
-rw-r--r-- | test/unittest_lint.py | 156 | ||||
-rw-r--r-- | utils.py | 188 |
6 files changed, 202 insertions, 179 deletions
@@ -50,7 +50,7 @@ from astroid.modutils import load_module_from_name, get_module_part from pylint.utils import ( MSG_TYPES, OPTION_RGX, PyLintASTWalker, UnknownMessage, MessagesHandlerMixIn, ReportsHandlerMixIn, - EmptyReport, WarningScope, + MessagesStore, EmptyReport, WarningScope, expand_modules, tokenize_module) from pylint.interfaces import IRawChecker, ITokenChecker, IAstroidChecker from pylint.checkers import (BaseTokenChecker, @@ -286,7 +286,8 @@ warning, statement which respectively contain the number of errors / warnings\ pylintrc=None): # some stuff has to be done before ancestors initialization... # - # checkers / reporter / astroid manager + # messages store / checkers / reporter / astroid manager + self.msgs_store = MessagesStore() self.reporter = None self._reporter_name = None self._reporters = {} @@ -423,11 +424,11 @@ warning, statement which respectively contain the number of errors / warnings\ self.register_report(r_id, r_title, r_cb, checker) self.register_options_provider(checker) if hasattr(checker, 'msgs'): - self.register_messages(checker) + self.msgs_store.register_messages(checker) checker.load_defaults() def disable_noerror_messages(self): - for msgcat, msgids in self._msgs_by_category.iteritems(): + for msgcat, msgids in self.msgs_store._msgs_by_category.iteritems(): if msgcat == 'E': for msgid in msgids: self.enable(msgid) @@ -529,7 +530,7 @@ warning, statement which respectively contain the number of errors / warnings\ if first <= lineno <= last: # Set state for all lines for this block, if the # warning is applied to nodes. - if self.check_message_id(msgid).scope == WarningScope.NODE: + if self.msgs_store.check_message_id(msgid).scope == WarningScope.NODE: if lineno > firstchildlineno: state = True first_, last_ = node.block_range(lineno) @@ -596,6 +597,12 @@ warning, statement which respectively contain the number of errors / warnings\ """main checking entry: check a list of files or modules from their name. """ + # initialize msgs_state now that all messages have been registered into + # the store + for msg in self.msgs_store.messages: + if not msg.may_be_emitted(): + self._msgs_state[msg.msgid] = False + if not isinstance(files_or_modules, (list, tuple)): files_or_modules = (files_or_modules,) walker = PyLintASTWalker(self) @@ -758,12 +765,12 @@ warning, statement which respectively contain the number of errors / warnings\ for line, enable in lines.iteritems(): if not enable and (warning, line) not in self._ignored_msgs: self.add_message('useless-suppression', line, None, - (self.get_msg_display_string(warning),)) + (self.msgs_store.get_msg_display_string(warning),)) # don't use iteritems here, _ignored_msgs may be modified by add_message for (warning, from_), lines in self._ignored_msgs.items(): for line in lines: self.add_message('suppressed-message', line, None, - (self.get_msg_display_string(warning), from_)) + (self.msgs_store.get_msg_display_string(warning), from_)) def report_evaluation(self, sect, stats, previous_stats): """make the global evaluation report""" @@ -1088,7 +1095,7 @@ are done by default'''}), def cb_help_message(self, option, optname, value, parser): """optik callback for printing some help about a particular message""" - self.linter.help_message(splitstrip(value)) + self.linter.msgs_store.help_message(splitstrip(value)) sys.exit(0) def cb_full_documentation(self, option, optname, value, parser): @@ -1098,7 +1105,7 @@ are done by default'''}), def cb_list_messages(self, option, optname, value, parser): # FIXME """optik callback for printing available messages""" - self.linter.list_messages() + self.linter.msgs_store.list_messages() sys.exit(0) def cb_init_hook(optname, value): diff --git a/reporters/__init__.py b/reporters/__init__.py index a767a05..12d193f 100644 --- a/reporters/__init__.py +++ b/reporters/__init__.py @@ -51,7 +51,7 @@ class Message(object): self.msg = msg self.C = msg_id[0] self.category = MSG_TYPES[msg_id[0]] - self.symbol = reporter.linter.check_message_id(msg_id).symbol + self.symbol = reporter.linter.msgs_store.check_message_id(msg_id).symbol def format(self, template): """Format the message according to the given template. diff --git a/test/test_func.py b/test/test_func.py index 6a64ec2..2d573c2 100644 --- a/test/test_func.py +++ b/test/test_func.py @@ -1,4 +1,4 @@ -# Copyright (c) 2003-2008 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2003-2014 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -45,11 +45,11 @@ class LintTestNonExistentModuleTC(LintTestUsingModule): class TestTests(testlib.TestCase): """check that all testable messages have been checked""" PORTED = set(['I0001', 'I0010', 'W0712', 'E1001', 'W1402', 'E1310']) - + @testlib.tag('coverage') def test_exhaustivity(self): # skip fatal messages - not_tested = set(msg.msgid for msg in linter.messages + not_tested = set(msg.msgid for msg in linter.msgs_store.messages if msg.msgid[0] != 'F' and msg.may_be_emitted()) for msgid in test_reporter.message_ids: try: diff --git a/test/test_regr.py b/test/test_regr.py index 0349481..8bc50c2 100644 --- a/test/test_regr.py +++ b/test/test_regr.py @@ -1,4 +1,4 @@ -# Copyright (c) 2005 LOGILAB S.A. (Paris, FRANCE). +# Copyright (c) 2005-2014 LOGILAB S.A. (Paris, FRANCE). # http://www.logilab.fr/ -- mailto:contact@logilab.fr # # This program is free software; you can redistribute it and/or modify it under @@ -60,7 +60,7 @@ class NonRegrTC(TestCase): got = linter.reporter.finalize().strip() checked = linter.stats['by_module'].keys() self.assertEqual(checked, ['package.__init__'], - '%s: %s' % (variation, checked)) + '%s: %s' % (variation, checked)) cwd = os.getcwd() os.chdir(join(REGR_DATA, 'package')) sys.path.insert(0, '') diff --git a/test/unittest_lint.py b/test/unittest_lint.py index a856d73..0e0ed9c 100644 --- a/test/unittest_lint.py +++ b/test/unittest_lint.py @@ -27,7 +27,8 @@ from pylint import config from pylint.lint import PyLinter, Run, UnknownMessage, preprocess_options, \ ArgumentPreprocessingError from pylint.utils import MSG_STATE_SCOPE_CONFIG, MSG_STATE_SCOPE_MODULE, \ - PyLintASTWalker, MessageDefinition, build_message_def, tokenize_module + MessagesStore, PyLintASTWalker, MessageDefinition, build_message_def, \ + tokenize_module from pylint.testutils import TestReporter from pylint.reporters import text from pylint import checkers @@ -60,51 +61,6 @@ class PyLinterTC(TestCase): checkers.initialize(self.linter) self.linter.set_reporter(TestReporter()) - def _compare_messages(self, desc, msg, checkerref=False): - # replace \r\n with \n, because - # logilab.common.textutils.normalize_text - # uses os.linesep, which will - # not properly compare with triple - # quoted multilines used in these tests - self.assertMultiLineEqual(desc, - msg.format_help(checkerref=checkerref) - .replace('\r\n', '\n')) - - def test_check_message_id(self): - self.assertIsInstance(self.linter.check_message_id('F0001'), - MessageDefinition) - self.assertRaises(UnknownMessage, - self.linter.check_message_id, 'YB12') - - def test_message_help(self): - msg = self.linter.check_message_id('F0001') - self._compare_messages( - ''':fatal (F0001): - Used when an error occurred preventing the analysis of a module (unable to - find it for instance). This message belongs to the master checker.''', - msg, checkerref=True) - self._compare_messages( - ''':fatal (F0001): - Used when an error occurred preventing the analysis of a module (unable to - find it for instance).''', - msg, checkerref=False) - - def test_message_help_minmax(self): - # build the message manually to be python version independant - msg = build_message_def(self.linter._checkers['typecheck'][0], - 'E1122', checkers.typecheck.MSGS['E1122']) - self._compare_messages( - ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in %s call* - Used when a function call passes the same keyword argument multiple times. - This message belongs to the typecheck checker. It can't be emitted when using - Python >= 2.6.''', - msg, checkerref=True) - self._compare_messages( - ''':duplicate-keyword-arg (E1122): *Duplicate keyword argument %r in %s call* - Used when a function call passes the same keyword argument multiple times. - This message can't be emitted when using Python >= 2.6.''', - msg, checkerref=False) - def test_enable_message(self): linter = self.linter linter.open() @@ -246,16 +202,6 @@ class PyLinterTC(TestCase): self.assertTrue(linter.is_message_enabled('W0102', 1)) self.assertTrue(linter.is_message_enabled('dangerous-default-value', 1)) - def test_list_messages(self): - sys.stdout = StringIO() - try: - self.linter.list_messages() - output = sys.stdout.getvalue() - finally: - sys.stdout = sys.__stdout__ - # cursory examination of the output: we're mostly testing it completes - self.assertIn(':empty-docstring (C0112): *Empty %s docstring*', output) - def test_lint_ext_module_with_file_output(self): self.linter.set_reporter(text.TextReporter()) if sys.version_info < (3, 0): @@ -363,23 +309,6 @@ class PyLinterTC(TestCase): ['C: 1: Line too long (1/2)', 'C: 2: Line too long (3/4)'], self.linter.reporter.messages) - def test_add_renamed_message(self): - self.linter.add_renamed_message('C9999', 'old-bad-name', 'invalid-name') - self.assertEqual('invalid-name', - self.linter.check_message_id('C9999').symbol) - self.assertEqual('invalid-name', - self.linter.check_message_id('old-bad-name').symbol) - - def test_renamed_message_register(self): - class Checker(object): - msgs = {'W1234': ('message', 'msg-symbol', 'msg-description', - {'old_names': [('W0001', 'old-symbol')]})} - self.linter.register_messages(Checker()) - self.assertEqual('msg-symbol', - self.linter.check_message_id('W0001').symbol) - self.assertEqual('msg-symbol', - self.linter.check_message_id('old-symbol').symbol) - def test_init_hooks_called_before_load_plugins(self): self.assertRaises(RuntimeError, Run, ['--load-plugins', 'unexistant', '--init-hook', 'raise RuntimeError']) @@ -541,5 +470,86 @@ class PreprocessOptionsTC(TestCase): {'bar' : (None, False)}) +class MessagesStoreTC(TestCase): + def setUp(self): + self.store = MessagesStore() + class Checker(object): + name = 'achecker' + msgs = { + 'W1234': ('message', 'msg-symbol', 'msg description.', + {'old_names': [('W0001', 'old-symbol')]}), + 'E1234': ('Duplicate keyword argument %r in %s call', + 'duplicate-keyword-arg', + 'Used when a function call passes the same keyword argument multiple times.', + {'maxversion': (2, 6)}), + } + self.store.register_messages(Checker()) + + def _compare_messages(self, desc, msg, checkerref=False): + # replace \r\n with \n, because + # logilab.common.textutils.normalize_text + # uses os.linesep, which will + # not properly compare with triple + # quoted multilines used in these tests + self.assertMultiLineEqual( + desc, + msg.format_help(checkerref=checkerref).replace('\r\n', '\n')) + + def test_check_message_id(self): + self.assertIsInstance(self.store.check_message_id('W1234'), + MessageDefinition) + self.assertRaises(UnknownMessage, + self.store.check_message_id, 'YB12') + + def test_message_help(self): + msg = self.store.check_message_id('W1234') + self._compare_messages( + ''':msg-symbol (W1234): *message* + msg description. This message belongs to the achecker checker.''', + msg, checkerref=True) + self._compare_messages( + ''':msg-symbol (W1234): *message* + msg description.''', + msg, checkerref=False) + + def test_message_help_minmax(self): + # build the message manually to be python version independant + msg = self.store.check_message_id('E1234') + self._compare_messages( + ''':duplicate-keyword-arg (E1234): *Duplicate keyword argument %r in %s call* + Used when a function call passes the same keyword argument multiple times. + This message belongs to the achecker checker. It can't be emitted when using + Python >= 2.6.''', + msg, checkerref=True) + self._compare_messages( + ''':duplicate-keyword-arg (E1234): *Duplicate keyword argument %r in %s call* + Used when a function call passes the same keyword argument multiple times. + This message can't be emitted when using Python >= 2.6.''', + msg, checkerref=False) + + def test_list_messages(self): + sys.stdout = StringIO() + try: + self.store.list_messages() + output = sys.stdout.getvalue() + finally: + sys.stdout = sys.__stdout__ + # cursory examination of the output: we're mostly testing it completes + self.assertIn(':msg-symbol (W1234): *message*', output) + + def test_add_renamed_message(self): + self.store.add_renamed_message('W1234', 'old-bad-name', 'msg-symbol') + self.assertEqual('msg-symbol', + self.store.check_message_id('W1234').symbol) + self.assertEqual('msg-symbol', + self.store.check_message_id('old-bad-name').symbol) + + def test_renamed_message_register(self): + self.assertEqual('msg-symbol', + self.store.check_message_id('W0001').symbol) + self.assertEqual('msg-symbol', + self.store.check_message_id('old-symbol').symbol) + + if __name__ == '__main__': unittest_main() @@ -190,62 +190,13 @@ class MessagesHandlerMixIn(object): """ def __init__(self): - # Primary registry for all active messages (i.e. all messages - # that can be emitted by pylint for the underlying Python - # version). It contains the 1:1 mapping from symbolic names - # to message definition objects. - self._messages = {} - # Maps alternative names (numeric IDs, deprecated names) to - # message definitions. May contain several names for each definition - # object. - self._alternative_names = {} self._msgs_state = {} self._module_msgs_state = {} # None self._raw_module_msgs_state = {} - self._msgs_by_category = {} self.msg_status = 0 self._ignored_msgs = {} self._suppression_mapping = {} - def add_renamed_message(self, old_id, old_symbol, new_symbol): - """Register the old ID and symbol for a warning that was renamed. - - This allows users to keep using the old ID/symbol in suppressions. - """ - msg = self.check_message_id(new_symbol) - msg.old_names.append((old_id, old_symbol)) - self._alternative_names[old_id] = msg - self._alternative_names[old_symbol] = msg - - def register_messages(self, checker): - """register a dictionary of messages - - Keys are message ids, values are a 2-uple with the message type and the - message itself - - message ids should be a string of len 4, where the two first characters - are the checker id and the two last the message id in this checker - """ - chkid = None - for msgid, msg_tuple in checker.msgs.iteritems(): - msg = build_message_def(checker, msgid, msg_tuple) - assert msg.symbol not in self._messages, \ - 'Message symbol %r is already defined' % msg.symbol - # avoid duplicate / malformed ids - assert msg.msgid not in self._alternative_names, \ - 'Message id %r is already defined' % msgid - assert chkid is None or chkid == msg.msgid[1:3], \ - 'Inconsistent checker part in message id %r' % msgid - chkid = msg.msgid[1:3] - if not msg.may_be_emitted(): - self._msgs_state[msg.msgid] = False - self._messages[msg.symbol] = msg - self._alternative_names[msg.msgid] = msg - for old_id, old_symbol in msg.old_names: - self._alternative_names[old_id] = msg - self._alternative_names[old_symbol] = msg - self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid) - def disable(self, msgid, scope='package', line=None, ignore_unknown=False): """don't output message of the given id""" assert scope in ('package', 'module') @@ -257,14 +208,15 @@ class MessagesHandlerMixIn(object): # msgid is a category? catid = category_id(msgid) if catid is not None: - for _msgid in self._msgs_by_category.get(catid): + for _msgid in self.msgs_store._msgs_by_category.get(catid): self.disable(_msgid, scope, line) return # msgid is a checker name? if msgid.lower() in self._checkers: + msgs_store = self.msgs_store for checker in self._checkers[msgid.lower()]: for _msgid in checker.msgs: - if _msgid in self._alternative_names: + if _msgid in msgs_store._alternative_names: self.disable(_msgid, scope, line) return # msgid is report id? @@ -274,7 +226,7 @@ class MessagesHandlerMixIn(object): try: # msgid is a symbolic or numeric msgid. - msg = self.check_message_id(msgid) + msg = self.msgs_store.check_message_id(msgid) except UnknownMessage: if ignore_unknown: return @@ -303,7 +255,7 @@ class MessagesHandlerMixIn(object): catid = category_id(msgid) # msgid is a category? if catid is not None: - for msgid in self._msgs_by_category.get(catid): + for msgid in self.msgs_store._msgs_by_category.get(catid): self.enable(msgid, scope, line) return # msgid is a checker name? @@ -319,7 +271,7 @@ class MessagesHandlerMixIn(object): try: # msgid is a symbolic or numeric msgid. - msg = self.check_message_id(msgid) + msg = self.msgs_store.check_message_id(msgid) except UnknownMessage: if ignore_unknown: return @@ -337,30 +289,6 @@ class MessagesHandlerMixIn(object): msgs[msg.msgid] = True # sync configuration object self.config.enable = [mid for mid, val in msgs.iteritems() if val] - - def check_message_id(self, msgid): - """returns the Message object for this message. - - msgid may be either a numeric or symbolic id. - - Raises UnknownMessage if the message id is not defined. - """ - if msgid[1:].isdigit(): - msgid = msgid.upper() - for source in (self._alternative_names, self._messages): - try: - return source[msgid] - except KeyError: - pass - raise UnknownMessage('No such message id %s' % msgid) - - def get_msg_display_string(self, msgid): - """Generates a user-consumable representation of a message. - - Can be just the message ID or the ID and the symbol. - """ - return repr(self.check_message_id(msgid).symbol) - def get_message_state_scope(self, msgid, line=None): """Returns the scope at which a message was enabled/disabled.""" try: @@ -376,7 +304,7 @@ class MessagesHandlerMixIn(object): msgid may be either a numeric or symbolic message id. """ try: - msgid = self.check_message_id(msg_descr).msgid + msgid = self.msgs_store.check_message_id(msg_descr).msgid except UnknownMessage: # The linter checks for messages that are not registered # due to version mismatch, just treat them as message IDs @@ -412,7 +340,7 @@ class MessagesHandlerMixIn(object): provide line if the line number is different), raw and token checkers must provide the line argument. """ - msg_info = self.check_message_id(msg_descr) + msg_info = self.msgs_store.check_message_id(msg_descr) msgid = msg_info.msgid # backward compatibility, message may not have a symbol symbol = msg_info.symbol or msgid @@ -460,17 +388,6 @@ class MessagesHandlerMixIn(object): # add the message self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offset or 0), msg) - def help_message(self, msgids): - """display help messages for the given message identifiers""" - for msgid in msgids: - try: - print self.check_message_id(msgid).format_help(checkerref=True) - print - except UnknownMessage, ex: - print ex - print - continue - def print_full_documentation(self): """output a full documentation in ReST format""" by_checker = {} @@ -528,11 +445,100 @@ class MessagesHandlerMixIn(object): print print + +class MessagesStore(object): + """The messages store knows information about every possible message but has + no particular state during analysis. + """ + + def __init__(self): + # Primary registry for all active messages (i.e. all messages + # that can be emitted by pylint for the underlying Python + # version). It contains the 1:1 mapping from symbolic names + # to message definition objects. + self._messages = {} + # Maps alternative names (numeric IDs, deprecated names) to + # message definitions. May contain several names for each definition + # object. + self._alternative_names = {} + self._msgs_by_category = {} + @property def messages(self): """The list of all active messages.""" return self._messages.itervalues() + def add_renamed_message(self, old_id, old_symbol, new_symbol): + """Register the old ID and symbol for a warning that was renamed. + + This allows users to keep using the old ID/symbol in suppressions. + """ + msg = self.check_message_id(new_symbol) + msg.old_names.append((old_id, old_symbol)) + self._alternative_names[old_id] = msg + self._alternative_names[old_symbol] = msg + + def register_messages(self, checker): + """register a dictionary of messages + + Keys are message ids, values are a 2-uple with the message type and the + message itself + + message ids should be a string of len 4, where the two first characters + are the checker id and the two last the message id in this checker + """ + chkid = None + for msgid, msg_tuple in checker.msgs.iteritems(): + msg = build_message_def(checker, msgid, msg_tuple) + assert msg.symbol not in self._messages, \ + 'Message symbol %r is already defined' % msg.symbol + # avoid duplicate / malformed ids + assert msg.msgid not in self._alternative_names, \ + 'Message id %r is already defined' % msgid + assert chkid is None or chkid == msg.msgid[1:3], \ + 'Inconsistent checker part in message id %r' % msgid + chkid = msg.msgid[1:3] + self._messages[msg.symbol] = msg + self._alternative_names[msg.msgid] = msg + for old_id, old_symbol in msg.old_names: + self._alternative_names[old_id] = msg + self._alternative_names[old_symbol] = msg + self._msgs_by_category.setdefault(msg.msgid[0], []).append(msg.msgid) + + def check_message_id(self, msgid): + """returns the Message object for this message. + + msgid may be either a numeric or symbolic id. + + Raises UnknownMessage if the message id is not defined. + """ + if msgid[1:].isdigit(): + msgid = msgid.upper() + for source in (self._alternative_names, self._messages): + try: + return source[msgid] + except KeyError: + pass + raise UnknownMessage('No such message id %s' % msgid) + + def get_msg_display_string(self, msgid): + """Generates a user-consumable representation of a message. + + Can be just the message ID or the ID and the symbol. + """ + return repr(self.check_message_id(msgid).symbol) + + def help_message(self, msgids): + """display help messages for the given message identifiers""" + for msgid in msgids: + try: + print self.check_message_id(msgid).format_help(checkerref=True) + print + except UnknownMessage, ex: + print ex + print + continue + def list_messages(self): """output full messages list documentation in ReST format""" msgs = sorted(self._messages.itervalues(), key=lambda msg: msg.msgid) |