PageRenderTime 74ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/tools/pylint/utils.py

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