summaryrefslogtreecommitdiff
path: root/utils.py
diff options
context:
space:
mode:
authorEmile Anclin <emile.anclin@logilab.fr>2008-09-08 17:07:36 +0200
committerEmile Anclin <emile.anclin@logilab.fr>2008-09-08 17:07:36 +0200
commit349a72f0971eb022ab896e7fbf3d03a55315f7e3 (patch)
tree0dd42bad98c553724eab83fce3ed577d3c3d118d /utils.py
parentdc2c80d0e79e96d54f8e4d248b21518f4beddfab (diff)
parent559c2c0df87acf21103b9d489b648dc6e8359a3d (diff)
downloadpylint-git-349a72f0971eb022ab896e7fbf3d03a55315f7e3.tar.gz
merge pyreverse into pylint
Diffstat (limited to 'utils.py')
-rw-r--r--utils.py387
1 files changed, 387 insertions, 0 deletions
diff --git a/utils.py b/utils.py
new file mode 100644
index 000000000..022807cb8
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,387 @@
+# Copyright (c) 2003-2008 Sylvain Thenault (thenault@gmail.com).
+# Copyright (c) 2003-2008 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
+# 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
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc.,
+# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+"""some various utilities and helper classes, most of them used in the
+main pylint class
+"""
+
+import sys
+from os import linesep
+
+from logilab.common.textutils import normalize_text
+from logilab.common.configuration import rest_format_section
+from logilab.common.ureports import Section
+
+from logilab.astng import Module
+
+from pylint.checkers import EmptyReport
+
+class UnknownMessage(Exception):
+ """raised when a unregistered message id is encountered"""
+
+
+MSG_TYPES = {
+ 'I' : 'info',
+ 'C' : 'convention',
+ 'R' : 'refactor',
+ 'W' : 'warning',
+ 'E' : 'error',
+ 'F' : 'fatal'
+ }
+MSG_CATEGORIES = MSG_TYPES.keys()
+
+
+def sort_checkers(checkers, enabled_only=True):
+ """return a list of enabled checker sorted by priority"""
+ if enabled_only:
+ checkers = [(-checker.priority, checker) for checker in checkers
+ if checker.is_enabled()]
+ else:
+ checkers = [(-checker.priority, checker) for checker in checkers]
+ checkers.sort()
+ return [item[1] for item in checkers]
+
+def sort_msgs(msg_ids):
+ """sort message identifiers according to their category first"""
+ msg_order = ['E', 'W', 'R', 'C', 'I', 'F']
+ def cmp_func(msgid1, msgid2):
+ """comparison function for two message identifiers"""
+ if msgid1[0] != msgid2[0]:
+ return cmp(msg_order.index(msgid1[0]), msg_order.index(msgid2[0]))
+ else:
+ return cmp(msgid1, msgid2)
+ msg_ids.sort(cmp_func)
+ return msg_ids
+
+def get_module_and_frameid(node):
+ """return the module name and the frame id in the module"""
+ frame = node.frame()
+ module, obj = '', []
+ while frame:
+ if isinstance(frame, Module):
+ module = frame.name
+ else:
+ obj.append(getattr(frame, 'name', '<lambda>'))
+ try:
+ frame = frame.parent.frame()
+ except AttributeError:
+ frame = None
+ obj.reverse()
+ return module, '.'.join(obj)
+
+
+class Message:
+ def __init__(self, checker, msgid, msg, descr):
+ assert len(msgid) == 5, 'Invalid message id %s' % msgid
+ assert msgid[0] in MSG_CATEGORIES, \
+ 'Bad message type %s in %r' % (msgid[0], msgid)
+ self.msgid = msgid
+ self.msg = msg
+ self.descr = descr
+ self.checker = checker
+
+class MessagesHandlerMixIn:
+ """a mix-in class containing all the messages related methods for the main
+ lint class
+ """
+
+ def __init__(self):
+ # dictionary of registered messages
+ self._messages = {}
+ self._msgs_state = {}
+ self._module_msgs_state = {} # None
+ self._msg_cats_state = {}
+ self._module_msg_cats_state = None
+
+ 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 to first characters
+ are the checker id and the two last the message id in this checker
+ """
+ msgs_dict = checker.msgs
+ chkid = None
+ for msgid, (msg, msgdescr) in msgs_dict.items():
+ # avoid duplicate / malformed ids
+ assert not self._messages.has_key(msgid), \
+ 'Message id %r is already defined' % msgid
+ assert chkid is None or chkid == msgid[1:3], \
+ 'Inconsistent checker part in message id %r' % msgid
+ chkid = msgid[1:3]
+ self._messages[msgid] = Message(checker, msgid, msg, msgdescr)
+
+ def get_message_help(self, msg_id, checkerref=False):
+ """return the help string for the given message id"""
+ msg = self.check_message_id(msg_id)
+ desc = normalize_text(' '.join(msg.descr.split()), indent=' ')
+ if checkerref:
+ desc += ' This message belongs to the %s checker.' % \
+ msg.checker.name
+ title = msg.msg
+ if title != '%s':
+ title = title.splitlines()[0]
+ return ':%s: *%s*\n%s' % (msg.msgid, title, desc)
+ return ':%s:\n%s' % (msg.msgid, desc)
+
+ def disable_message(self, msg_id, scope='package', line=None):
+ """don't output message of the given id"""
+ assert scope in ('package', 'module')
+ msg = self.check_message_id(msg_id)
+ 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_id != 'I0011':
+ self.add_message('I0011', line=line, args=msg.msgid)
+
+ else:
+ msgs = self._msgs_state
+ msgs[msg.msgid] = False
+ # sync configuration object
+ self.config.disable_msg = [mid for mid, val in msgs.items()
+ if not val]
+
+ def enable_message(self, msg_id, scope='package', line=None):
+ """reenable message of the given id"""
+ assert scope in ('package', 'module')
+ msg = self.check_message_id(msg_id)
+ msg.checker.enabled = True # ensure the related checker is enabled
+ 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('I0012', line=line, args=msg.msgid)
+ else:
+ msgs = self._msgs_state
+ msgs[msg.msgid] = True
+ # sync configuration object
+ self.config.enable_msg = [mid for mid, val in msgs.items() if val]
+
+ def disable_message_category(self, msg_cat_id, scope='package', line=None):
+ """don't output message in the given category"""
+ assert scope in ('package', 'module')
+ msg_cat_id = msg_cat_id[0].upper()
+ if scope == 'module':
+ self.add_message('I0011', line=line, args=msg_cat_id)
+ self._module_msg_cats_state[msg_cat_id] = False
+ else:
+ self._msg_cats_state[msg_cat_id] = False
+
+ def enable_message_category(self, msg_cat_id, scope='package', line=None):
+ """reenable message of the given category"""
+ assert scope in ('package', 'module')
+ msg_cat_id = msg_cat_id[0].upper()
+ if scope == 'module':
+ self.add_message('I0012', line=line, args=msg_cat_id)
+ self._module_msg_cats_state[msg_cat_id] = True
+ else:
+ self._msg_cats_state[msg_cat_id] = True
+
+ def check_message_id(self, msg_id):
+ """raise UnknownMessage if the message id is not defined"""
+ msg_id = msg_id.upper()
+ try:
+ return self._messages[msg_id]
+ except KeyError:
+ raise UnknownMessage('No such message id %s' % msg_id)
+
+ def is_message_enabled(self, msg_id, line=None):
+ """return true if the message associated to the given message id is
+ enabled
+ """
+ try:
+ if not self._module_msg_cats_state[msg_id[0]]:
+ return False
+ except (KeyError, TypeError):
+ if not self._msg_cats_state.get(msg_id[0], True):
+ return False
+ if line is None:
+ return self._msgs_state.get(msg_id, True)
+ try:
+ return self._module_msgs_state[msg_id][line]
+ except (KeyError, TypeError):
+ return self._msgs_state.get(msg_id, True)
+
+ def add_message(self, msg_id, line=None, node=None, args=None):
+ """add the message corresponding to the given id.
+
+ If provided, msg is expanded using args
+
+ astng checkers should provide the node argument, raw checkers should
+ provide the line argument.
+ """
+ if line is None and node is not None:
+ line = node.fromlineno#lineno or node.statement().lineno
+ #if not isinstance(node, Module):
+ # assert line > 0, node.__class__
+ # should this message be displayed
+ if not self.is_message_enabled(msg_id, line):
+ return
+ # update stats
+ msg_cat = MSG_TYPES[msg_id[0]]
+ self.stats[msg_cat] += 1
+ self.stats['by_module'][self.current_name][msg_cat] += 1
+ try:
+ self.stats['by_msg'][msg_id] += 1
+ except KeyError:
+ self.stats['by_msg'][msg_id] = 1
+ msg = self._messages[msg_id].msg
+ # expand message ?
+ if args:
+ msg %= args
+ # get module and object
+ if node is None:
+ module, obj = self.current_name, ''
+ path = self.current_file
+ else:
+ module, obj = get_module_and_frameid(node)
+ path = node.root().file
+ # add the message
+ self.reporter.add_message(msg_id, (path, module, obj, line or 1), msg)
+
+ def help_message(self, msgids):
+ """display help messages for the given message identifiers"""
+ for msg_id in msgids:
+ try:
+ print self.get_message_help(msg_id, True)
+ print
+ except UnknownMessage, ex:
+ print ex
+ print
+ continue
+
+ def list_messages(self):
+ """output a full documentation in ReST format"""
+ for checker in sort_checkers(self._checkers.values()):
+ if checker.name == 'master':
+ prefix = 'Main '
+ if checker.options:
+ for section, options in checker.options_by_section():
+ if section is None:
+ title = 'General options'
+ else:
+ title = '%s options' % section.capitalize()
+ print title
+ print '~' * len(title)
+ rest_format_section(sys.stdout, None, options)
+ print
+ else:
+ prefix = ''
+ title = '%s checker' % checker.name.capitalize()
+ print title
+ print '-' * len(title)
+ if checker.__doc__: # __doc__ is None with -OO
+ print linesep.join([l.strip()
+ for l in checker.__doc__.splitlines()])
+ if checker.options:
+ title = 'Options'
+ print title
+ print '~' * len(title)
+ for section, options in checker.options_by_section():
+ rest_format_section(sys.stdout, section, options)
+ print
+ if checker.msgs:
+ title = ('%smessages' % prefix).capitalize()
+ print title
+ print '~' * len(title)
+ for msg_id in sort_msgs(checker.msgs.keys()):
+ print self.get_message_help(msg_id, False)
+ print
+ if getattr(checker, 'reports', None):
+ title = ('%sreports' % prefix).capitalize()
+ print title
+ print '~' * len(title)
+ for report in checker.reports:
+ print ':%s: %s' % report[:2]
+ print
+ print
+
+
+class ReportsHandlerMixIn:
+ """a mix-in class containing all the reports and stats manipulation
+ related methods for the main lint class
+ """
+ def __init__(self):
+ self._reports = {}
+ self._reports_state = {}
+
+ def register_report(self, r_id, r_title, r_cb, checker):
+ """register a report
+
+ r_id is the unique identifier for the report
+ r_title the report's title
+ r_cb the method to call to make the report
+ checker is the checker defining the report
+ """
+ r_id = r_id.upper()
+ self._reports.setdefault(checker, []).append( (r_id, r_title, r_cb) )
+
+ def enable_report(self, r_id):
+ """disable the report of the given id"""
+ r_id = r_id.upper()
+ self._reports_state[r_id] = True
+
+ def disable_report(self, r_id):
+ """disable the report of the given id"""
+ r_id = r_id.upper()
+ self._reports_state[r_id] = False
+
+ def is_report_enabled(self, r_id):
+ """return true if the report associated to the given identifier is
+ enabled
+ """
+ return self._reports_state.get(r_id, True)
+
+ def make_reports(self, stats, old_stats):
+ """render registered reports"""
+ if self.config.files_output:
+ filename = 'pylint_global.' + self.reporter.extension
+ self.reporter.set_output(open(filename, 'w'))
+ sect = Section('Report',
+ '%s statements analysed.'% (self.stats['statement']))
+ checkers = sort_checkers(self._reports.keys())
+ checkers.reverse()
+ for checker in checkers:
+ for r_id, r_title, r_cb in self._reports[checker]:
+ if not self.is_report_enabled(r_id):
+ continue
+ report_sect = Section(r_title)
+ try:
+ r_cb(report_sect, stats, old_stats)
+ except EmptyReport:
+ continue
+ report_sect.report_id = r_id
+ sect.append(report_sect)
+ self.reporter.display_results(sect)
+
+ def add_stats(self, **kwargs):
+ """add some stats entries to the statistic dictionary
+ raise an AssertionError if there is a key conflict
+ """
+ for key, value in kwargs.items():
+ if key[-1] == '_':
+ key = key[:-1]
+ assert not self.stats.has_key(key)
+ self.stats[key] = value
+ return self.stats
+