Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(65)

Side by Side Diff: third_party/pylint/utils.py

Issue 10447014: Add pylint to depot_tools. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fix unittests. Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/pylint/reporters/text.py ('k') | third_party/setuptools/LICENSE.txt » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2003-2010 Sylvain Thenault (thenault@gmail.com).
2 # Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
3 # http://www.logilab.fr/ -- mailto:contact@logilab.fr
4 #
5 # This program is free software; you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 """some various utilities and helper classes, most of them used in the
18 main pylint class
19 """
20
21 import sys
22 from os import linesep
23 from os.path import dirname, basename, splitext, exists, isdir, join, normpath
24
25 from logilab.common.modutils import modpath_from_file, get_module_files, \
26 file_from_modpath
27 from logilab.common.textutils import normalize_text
28 from logilab.common.configuration import rest_format_section
29 from logilab.common.ureports import Section
30
31 from logilab.astng import nodes, Module
32
33 from pylint.checkers import EmptyReport
34
35
36 class UnknownMessage(Exception):
37 """raised when a unregistered message id is encountered"""
38
39
40 MSG_TYPES = {
41 'I' : 'info',
42 'C' : 'convention',
43 'R' : 'refactor',
44 'W' : 'warning',
45 'E' : 'error',
46 'F' : 'fatal'
47 }
48 MSG_TYPES_LONG = dict([(v, k) for k, v in MSG_TYPES.iteritems()])
49
50 MSG_TYPES_STATUS = {
51 'I' : 0,
52 'C' : 16,
53 'R' : 8,
54 'W' : 4,
55 'E' : 2,
56 'F' : 1
57 }
58
59 _MSG_ORDER = 'EWRCIF'
60
61 def sort_msgs(msgids):
62 """sort message identifiers according to their category first"""
63 msgs = {}
64 for msg in msgids:
65 msgs.setdefault(msg[0], []).append(msg)
66 result = []
67 for m_id in _MSG_ORDER:
68 if m_id in msgs:
69 result.extend( sorted(msgs[m_id]) )
70 return result
71
72 def get_module_and_frameid(node):
73 """return the module name and the frame id in the module"""
74 frame = node.frame()
75 module, obj = '', []
76 while frame:
77 if isinstance(frame, Module):
78 module = frame.name
79 else:
80 obj.append(getattr(frame, 'name', '<lambda>'))
81 try:
82 frame = frame.parent.frame()
83 except AttributeError:
84 frame = None
85 obj.reverse()
86 return module, '.'.join(obj)
87
88 def category_id(id):
89 id = id.upper()
90 if id in MSG_TYPES:
91 return id
92 return MSG_TYPES_LONG.get(id)
93
94
95 class Message:
96 def __init__(self, checker, msgid, msg, descr):
97 assert len(msgid) == 5, 'Invalid message id %s' % msgid
98 assert msgid[0] in MSG_TYPES, \
99 'Bad message type %s in %r' % (msgid[0], msgid)
100 self.msgid = msgid
101 self.msg = msg
102 self.descr = descr
103 self.checker = checker
104
105 class MessagesHandlerMixIn:
106 """a mix-in class containing all the messages related methods for the main
107 lint class
108 """
109
110 def __init__(self):
111 # dictionary of registered messages
112 self._messages = {}
113 self._msgs_state = {}
114 self._module_msgs_state = {} # None
115 self._msgs_by_category = {}
116 self.msg_status = 0
117
118 def register_messages(self, checker):
119 """register a dictionary of messages
120
121 Keys are message ids, values are a 2-uple with the message type and the
122 message itself
123
124 message ids should be a string of len 4, where the two first characters
125 are the checker id and the two last the message id in this checker
126 """
127 msgs_dict = checker.msgs
128 chkid = None
129 for msgid, (msg, msgdescr) in msgs_dict.items():
130 # avoid duplicate / malformed ids
131 assert msgid not in self._messages, \
132 'Message id %r is already defined' % msgid
133 assert chkid is None or chkid == msgid[1:3], \
134 'Inconsistent checker part in message id %r' % msgid
135 chkid = msgid[1:3]
136 self._messages[msgid] = Message(checker, msgid, msg, msgdescr)
137 self._msgs_by_category.setdefault(msgid[0], []).append(msgid)
138
139 def get_message_help(self, msgid, checkerref=False):
140 """return the help string for the given message id"""
141 msg = self.check_message_id(msgid)
142 desc = normalize_text(' '.join(msg.descr.split()), indent=' ')
143 if checkerref:
144 desc += ' This message belongs to the %s checker.' % \
145 msg.checker.name
146 title = msg.msg
147 if title != '%s':
148 title = title.splitlines()[0]
149 return ':%s: *%s*\n%s' % (msg.msgid, title, desc)
150 return ':%s:\n%s' % (msg.msgid, desc)
151
152 def disable(self, msgid, scope='package', line=None):
153 """don't output message of the given id"""
154 assert scope in ('package', 'module')
155 # msgid is a category?
156 catid = category_id(msgid)
157 if catid is not None:
158 for msgid in self._msgs_by_category.get(catid):
159 self.disable(msgid, scope, line)
160 return
161 # msgid is a checker name?
162 if msgid.lower() in self._checkers:
163 for checker in self._checkers[msgid.lower()]:
164 for msgid in checker.msgs:
165 self.disable(msgid, scope, line)
166 return
167 # msgid is report id?
168 if msgid.lower().startswith('rp'):
169 self.disable_report(msgid)
170 return
171 # msgid is a msgid.
172 msg = self.check_message_id(msgid)
173 if scope == 'module':
174 assert line > 0
175 try:
176 self._module_msgs_state[msg.msgid][line] = False
177 except KeyError:
178 self._module_msgs_state[msg.msgid] = {line: False}
179 if msgid != 'I0011':
180 self.add_message('I0011', line=line, args=msg.msgid)
181
182 else:
183 msgs = self._msgs_state
184 msgs[msg.msgid] = False
185 # sync configuration object
186 self.config.disable_msg = [mid for mid, val in msgs.items()
187 if not val]
188
189 def enable(self, msgid, scope='package', line=None):
190 """reenable message of the given id"""
191 assert scope in ('package', 'module')
192 catid = category_id(msgid)
193 # msgid is a category?
194 if catid is not None:
195 for msgid in self._msgs_by_category.get(catid):
196 self.enable(msgid, scope, line)
197 return
198 # msgid is a checker name?
199 if msgid.lower() in self._checkers:
200 for checker in self._checkers[msgid.lower()]:
201 for msgid in checker.msgs:
202 self.enable(msgid, scope, line)
203 return
204 # msgid is report id?
205 if msgid.lower().startswith('rp'):
206 self.enable_report(msgid)
207 return
208 # msgid is a msgid.
209 msg = self.check_message_id(msgid)
210 if scope == 'module':
211 assert line > 0
212 try:
213 self._module_msgs_state[msg.msgid][line] = True
214 except KeyError:
215 self._module_msgs_state[msg.msgid] = {line: True}
216 self.add_message('I0012', line=line, args=msg.msgid)
217 else:
218 msgs = self._msgs_state
219 msgs[msg.msgid] = True
220 # sync configuration object
221 self.config.enable = [mid for mid, val in msgs.items() if val]
222
223 def check_message_id(self, msgid):
224 """raise UnknownMessage if the message id is not defined"""
225 msgid = msgid.upper()
226 try:
227 return self._messages[msgid]
228 except KeyError:
229 raise UnknownMessage('No such message id %s' % msgid)
230
231 def is_message_enabled(self, msgid, line=None):
232 """return true if the message associated to the given message id is
233 enabled
234 """
235 if line is None:
236 return self._msgs_state.get(msgid, True)
237 try:
238 return self._module_msgs_state[msgid][line]
239 except (KeyError, TypeError):
240 return self._msgs_state.get(msgid, True)
241
242 def add_message(self, msgid, line=None, node=None, args=None):
243 """add the message corresponding to the given id.
244
245 If provided, msg is expanded using args
246
247 astng checkers should provide the node argument, raw checkers should
248 provide the line argument.
249 """
250 if line is None and node is not None:
251 line = node.fromlineno
252 if hasattr(node, 'col_offset'):
253 col_offset = node.col_offset # XXX measured in bytes for utf-8, divi de by two for chars?
254 else:
255 col_offset = None
256 # should this message be displayed
257 if not self.is_message_enabled(msgid, line):
258 return
259 # update stats
260 msg_cat = MSG_TYPES[msgid[0]]
261 self.msg_status |= MSG_TYPES_STATUS[msgid[0]]
262 self.stats[msg_cat] += 1
263 self.stats['by_module'][self.current_name][msg_cat] += 1
264 try:
265 self.stats['by_msg'][msgid] += 1
266 except KeyError:
267 self.stats['by_msg'][msgid] = 1
268 msg = self._messages[msgid].msg
269 # expand message ?
270 if args:
271 msg %= args
272 # get module and object
273 if node is None:
274 module, obj = self.current_name, ''
275 path = self.current_file
276 else:
277 module, obj = get_module_and_frameid(node)
278 path = node.root().file
279 # add the message
280 self.reporter.add_message(msgid, (path, module, obj, line or 1, col_offs et or 0), msg)
281
282 def help_message(self, msgids):
283 """display help messages for the given message identifiers"""
284 for msgid in msgids:
285 try:
286 print self.get_message_help(msgid, True)
287 print
288 except UnknownMessage, ex:
289 print ex
290 print
291 continue
292
293 def print_full_documentation(self):
294 """output a full documentation in ReST format"""
295 by_checker = {}
296 for checker in self.get_checkers():
297 if checker.name == 'master':
298 prefix = 'Main '
299 print "Options"
300 print '-------\n'
301 if checker.options:
302 for section, options in checker.options_by_section():
303 if section is None:
304 title = 'General options'
305 else:
306 title = '%s options' % section.capitalize()
307 print title
308 print '~' * len(title)
309 rest_format_section(sys.stdout, None, options)
310 print
311 else:
312 try:
313 by_checker[checker.name][0] += checker.options_and_values()
314 by_checker[checker.name][1].update(checker.msgs)
315 by_checker[checker.name][2] += checker.reports
316 except KeyError:
317 by_checker[checker.name] = [list(checker.options_and_values( )),
318 dict(checker.msgs),
319 list(checker.reports)]
320 for checker, (options, msgs, reports) in by_checker.items():
321 prefix = ''
322 title = '%s checker' % checker
323 print title
324 print '-' * len(title)
325 print
326 if options:
327 title = 'Options'
328 print title
329 print '~' * len(title)
330 rest_format_section(sys.stdout, None, options)
331 print
332 if msgs:
333 title = ('%smessages' % prefix).capitalize()
334 print title
335 print '~' * len(title)
336 for msgid in sort_msgs(msgs.keys()):
337 print self.get_message_help(msgid, False)
338 print
339 if reports:
340 title = ('%sreports' % prefix).capitalize()
341 print title
342 print '~' * len(title)
343 for report in reports:
344 print ':%s: %s' % report[:2]
345 print
346 print
347
348 def list_messages(self):
349 """output full messages list documentation in ReST format"""
350 msgids = []
351 for checker in self.get_checkers():
352 for msgid in checker.msgs.keys():
353 msgids.append(msgid)
354 msgids.sort()
355 for msgid in msgids:
356 print self.get_message_help(msgid, False)
357 print
358
359
360 class ReportsHandlerMixIn:
361 """a mix-in class containing all the reports and stats manipulation
362 related methods for the main lint class
363 """
364 def __init__(self):
365 self._reports = {}
366 self._reports_state = {}
367
368 def register_report(self, reportid, r_title, r_cb, checker):
369 """register a report
370
371 reportid is the unique identifier for the report
372 r_title the report's title
373 r_cb the method to call to make the report
374 checker is the checker defining the report
375 """
376 reportid = reportid.upper()
377 self._reports.setdefault(checker, []).append( (reportid, r_title, r_cb) )
378
379 def enable_report(self, reportid):
380 """disable the report of the given id"""
381 reportid = reportid.upper()
382 self._reports_state[reportid] = True
383
384 def disable_report(self, reportid):
385 """disable the report of the given id"""
386 reportid = reportid.upper()
387 self._reports_state[reportid] = False
388
389 def report_is_enabled(self, reportid):
390 """return true if the report associated to the given identifier is
391 enabled
392 """
393 return self._reports_state.get(reportid, True)
394
395 def make_reports(self, stats, old_stats):
396 """render registered reports"""
397 if self.config.files_output:
398 filename = 'pylint_global.' + self.reporter.extension
399 self.reporter.set_output(open(filename, 'w'))
400 sect = Section('Report',
401 '%s statements analysed.'% (self.stats['statement']))
402 for checker in self._reports:
403 for reportid, r_title, r_cb in self._reports[checker]:
404 if not self.report_is_enabled(reportid):
405 continue
406 report_sect = Section(r_title)
407 try:
408 r_cb(report_sect, stats, old_stats)
409 except EmptyReport:
410 continue
411 report_sect.report_id = reportid
412 sect.append(report_sect)
413 self.reporter.display_results(sect)
414
415 def add_stats(self, **kwargs):
416 """add some stats entries to the statistic dictionary
417 raise an AssertionError if there is a key conflict
418 """
419 for key, value in kwargs.items():
420 if key[-1] == '_':
421 key = key[:-1]
422 assert key not in self.stats
423 self.stats[key] = value
424 return self.stats
425
426
427 def expand_modules(files_or_modules, black_list):
428 """take a list of files/modules/packages and return the list of tuple
429 (file, module name) which have to be actually checked
430 """
431 result = []
432 errors = []
433 for something in files_or_modules:
434 if exists(something):
435 # this is a file or a directory
436 try:
437 modname = '.'.join(modpath_from_file(something))
438 except ImportError:
439 modname = splitext(basename(something))[0]
440 if isdir(something):
441 filepath = join(something, '__init__.py')
442 else:
443 filepath = something
444 else:
445 # suppose it's a module or package
446 modname = something
447 try:
448 filepath = file_from_modpath(modname.split('.'))
449 if filepath is None:
450 errors.append( {'key' : 'F0003', 'mod': modname} )
451 continue
452 except (ImportError, SyntaxError), ex:
453 # FIXME p3k : the SyntaxError is a Python bug and should be
454 # removed as soon as possible http://bugs.python.org/issue10588
455 errors.append( {'key': 'F0001', 'mod': modname, 'ex': ex} )
456 continue
457 filepath = normpath(filepath)
458 result.append( {'path': filepath, 'name': modname,
459 'basepath': filepath, 'basename': modname} )
460 if not (modname.endswith('.__init__') or modname == '__init__') \
461 and '__init__.py' in filepath:
462 for subfilepath in get_module_files(dirname(filepath), black_list):
463 if filepath == subfilepath:
464 continue
465 submodname = '.'.join(modpath_from_file(subfilepath))
466 result.append( {'path': subfilepath, 'name': submodname,
467 'basepath': filepath, 'basename': modname} )
468 return result, errors
469
470
471 class PyLintASTWalker(object):
472
473 def __init__(self, linter):
474 # callbacks per node types
475 self.nbstatements = 1
476 self.visit_events = {}
477 self.leave_events = {}
478 self.linter = linter
479
480 def add_checker(self, checker):
481 """walk to the checker's dir and collect visit and leave methods"""
482 # XXX : should be possible to merge needed_checkers and add_checker
483 vcids = set()
484 lcids = set()
485 visits = self.visit_events
486 leaves = self.leave_events
487 msgs = self.linter._msgs_state
488 for member in dir(checker):
489 cid = member[6:]
490 if cid == 'default':
491 continue
492 if member.startswith('visit_'):
493 v_meth = getattr(checker, member)
494 # don't use visit_methods with no activated message:
495 if hasattr(v_meth, 'checks_msgs'):
496 if not any(msgs.get(m, True) for m in v_meth.checks_msgs):
497 continue
498 visits.setdefault(cid, []).append(v_meth)
499 vcids.add(cid)
500 elif member.startswith('leave_'):
501 l_meth = getattr(checker, member)
502 # don't use leave_methods with no activated message:
503 if hasattr(l_meth, 'checks_msgs'):
504 if not any(msgs.get(m, True) for m in l_meth.checks_msgs):
505 continue
506 leaves.setdefault(cid, []).append(l_meth)
507 lcids.add(cid)
508 visit_default = getattr(checker, 'visit_default', None)
509 if visit_default:
510 for cls in nodes.ALL_NODE_CLASSES:
511 cid = cls.__name__.lower()
512 if cid not in vcids:
513 visits.setdefault(cid, []).append(visit_default)
514 # for now we have no "leave_default" method in Pylint
515
516 def walk(self, astng):
517 """call visit events of astng checkers for the given node, recurse on
518 its children, then leave events.
519 """
520 cid = astng.__class__.__name__.lower()
521 if astng.is_statement:
522 self.nbstatements += 1
523 # generate events for this node on each checker
524 for cb in self.visit_events.get(cid, ()):
525 cb(astng)
526 # recurse on children
527 for child in astng.get_children():
528 self.walk(child)
529 for cb in self.leave_events.get(cid, ()):
530 cb(astng)
531
OLDNEW
« no previous file with comments | « third_party/pylint/reporters/text.py ('k') | third_party/setuptools/LICENSE.txt » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698