imports.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. # Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
  2. # http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3. #
  4. # This program is free software; you can redistribute it and/or modify it under
  5. # the terms of the GNU General Public License as published by the Free Software
  6. # Foundation; either version 2 of the License, or (at your option) any later
  7. # version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along with
  14. # this program; if not, write to the Free Software Foundation, Inc.,
  15. # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  16. """imports checkers for Python code"""
  17. from logilab.common.graph import get_cycles, DotBackend
  18. from logilab.common.modutils import is_standard_module
  19. from logilab.common.ureports import VerbatimText, Paragraph
  20. from logilab import astng
  21. from logilab.astng import are_exclusive
  22. from pylint.interfaces import IASTNGChecker
  23. from pylint.checkers import BaseChecker, EmptyReport
  24. def get_first_import(node, context, name, base, level):
  25. """return the node where [base.]<name> is imported or None if not found
  26. """
  27. first = None
  28. found = False
  29. for first in context.values():
  30. if isinstance(first, astng.Import):
  31. if name in [iname[0] for iname in first.names]:
  32. found = True
  33. break
  34. elif isinstance(first, astng.From):
  35. if base == first.modname and level == first.level and \
  36. name in [iname[0] for iname in first.names]:
  37. found = True
  38. break
  39. if found and first is not node and not are_exclusive(first, node):
  40. return first
  41. # utilities to represents import dependencies as tree and dot graph ###########
  42. def filter_dependencies_info(dep_info, package_dir, mode='external'):
  43. """filter external or internal dependencies from dep_info (return a
  44. new dictionary containing the filtered modules only)
  45. """
  46. if mode == 'external':
  47. filter_func = lambda x: not is_standard_module(x, (package_dir,))
  48. else:
  49. assert mode == 'internal'
  50. filter_func = lambda x: is_standard_module(x, (package_dir,))
  51. result = {}
  52. for importee, importers in dep_info.items():
  53. if filter_func(importee):
  54. result[importee] = importers
  55. return result
  56. def make_tree_defs(mod_files_list):
  57. """get a list of 2-uple (module, list_of_files_which_import_this_module),
  58. it will return a dictionary to represent this as a tree
  59. """
  60. tree_defs = {}
  61. for mod, files in mod_files_list:
  62. node = (tree_defs, ())
  63. for prefix in mod.split('.'):
  64. node = node[0].setdefault(prefix, [{}, []])
  65. node[1] += files
  66. return tree_defs
  67. def repr_tree_defs(data, indent_str=None):
  68. """return a string which represents imports as a tree"""
  69. lines = []
  70. nodes = data.items()
  71. for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])):
  72. if not files:
  73. files = ''
  74. else:
  75. files = '(%s)' % ','.join(files)
  76. if indent_str is None:
  77. lines.append('%s %s' % (mod, files))
  78. sub_indent_str = ' '
  79. else:
  80. lines.append('%s\-%s %s' % (indent_str, mod, files))
  81. if i == len(nodes)-1:
  82. sub_indent_str = '%s ' % indent_str
  83. else:
  84. sub_indent_str = '%s| ' % indent_str
  85. if sub:
  86. lines.append(repr_tree_defs(sub, sub_indent_str))
  87. return '\n'.join(lines)
  88. def dependencies_graph(filename, dep_info):
  89. """write dependencies as a dot (graphviz) file
  90. """
  91. done = {}
  92. printer = DotBackend(filename[:-4], rankdir = "LR")
  93. printer.emit('URL="." node[shape="box"]')
  94. for modname, dependencies in dep_info.items():
  95. done[modname] = 1
  96. printer.emit_node(modname)
  97. for modname in dependencies:
  98. if modname not in done:
  99. done[modname] = 1
  100. printer.emit_node(modname)
  101. for depmodname, dependencies in dep_info.items():
  102. for modname in dependencies:
  103. printer.emit_edge(modname, depmodname)
  104. printer.generate(filename)
  105. def make_graph(filename, dep_info, sect, gtype):
  106. """generate a dependencies graph and add some information about it in the
  107. report's section
  108. """
  109. dependencies_graph(filename, dep_info)
  110. sect.append(Paragraph('%simports graph has been written to %s'
  111. % (gtype, filename)))
  112. # the import checker itself ###################################################
  113. MSGS = {
  114. 'F0401': ('Unable to import %s',
  115. 'Used when pylint has been unable to import a module.'),
  116. 'R0401': ('Cyclic import (%s)',
  117. 'Used when a cyclic import between two or more modules is \
  118. detected.'),
  119. 'W0401': ('Wildcard import %s',
  120. 'Used when `from module import *` is detected.'),
  121. 'W0402': ('Uses of a deprecated module %r',
  122. 'Used a module marked as deprecated is imported.'),
  123. 'W0403': ('Relative import %r, should be %r',
  124. 'Used when an import relative to the package directory is \
  125. detected.'),
  126. 'W0404': ('Reimport %r (imported line %s)',
  127. 'Used when a module is reimported multiple times.'),
  128. 'W0406': ('Module import itself',
  129. 'Used when a module is importing itself.'),
  130. 'W0410': ('__future__ import is not the first non docstring statement',
  131. 'Python 2.5 and greater require __future__ import to be the \
  132. first non docstring statement in the module.'),
  133. }
  134. class ImportsChecker(BaseChecker):
  135. """checks for
  136. * external modules dependencies
  137. * relative / wildcard imports
  138. * cyclic imports
  139. * uses of deprecated modules
  140. """
  141. __implements__ = IASTNGChecker
  142. name = 'imports'
  143. msgs = MSGS
  144. priority = -2
  145. options = (('deprecated-modules',
  146. {'default' : ('regsub', 'string', 'TERMIOS',
  147. 'Bastion', 'rexec'),
  148. 'type' : 'csv',
  149. 'metavar' : '<modules>',
  150. 'help' : 'Deprecated modules which should not be used, \
  151. separated by a comma'}
  152. ),
  153. ('import-graph',
  154. {'default' : '',
  155. 'type' : 'string',
  156. 'metavar' : '<file.dot>',
  157. 'help' : 'Create a graph of every (i.e. internal and \
  158. external) dependencies in the given file (report RP0402 must not be disabled)'}
  159. ),
  160. ('ext-import-graph',
  161. {'default' : '',
  162. 'type' : 'string',
  163. 'metavar' : '<file.dot>',
  164. 'help' : 'Create a graph of external dependencies in the \
  165. given file (report RP0402 must not be disabled)'}
  166. ),
  167. ('int-import-graph',
  168. {'default' : '',
  169. 'type' : 'string',
  170. 'metavar' : '<file.dot>',
  171. 'help' : 'Create a graph of internal dependencies in the \
  172. given file (report RP0402 must not be disabled)'}
  173. ),
  174. )
  175. def __init__(self, linter=None):
  176. BaseChecker.__init__(self, linter)
  177. self.stats = None
  178. self.import_graph = None
  179. self.__int_dep_info = self.__ext_dep_info = None
  180. self.reports = (('RP0401', 'External dependencies',
  181. self.report_external_dependencies),
  182. ('RP0402', 'Modules dependencies graph',
  183. self.report_dependencies_graph),
  184. )
  185. def open(self):
  186. """called before visiting project (i.e set of modules)"""
  187. self.linter.add_stats(dependencies={})
  188. self.linter.add_stats(cycles=[])
  189. self.stats = self.linter.stats
  190. self.import_graph = {}
  191. def close(self):
  192. """called before visiting project (i.e set of modules)"""
  193. # don't try to compute cycles if the associated message is disabled
  194. if self.linter.is_message_enabled('R0401'):
  195. for cycle in get_cycles(self.import_graph):
  196. self.add_message('R0401', args=' -> '.join(cycle))
  197. def visit_import(self, node):
  198. """triggered when an import statement is seen"""
  199. modnode = node.root()
  200. for name, _ in node.names:
  201. importedmodnode = self.get_imported_module(modnode, node, name)
  202. if importedmodnode is None:
  203. continue
  204. self._check_relative_import(modnode, node, importedmodnode, name)
  205. self._add_imported_module(node, importedmodnode.name)
  206. self._check_deprecated_module(node, name)
  207. self._check_reimport(node, name)
  208. def visit_from(self, node):
  209. """triggered when a from statement is seen"""
  210. basename = node.modname
  211. if basename == '__future__':
  212. # check if this is the first non-docstring statement in the module
  213. prev = node.previous_sibling()
  214. if prev:
  215. # consecutive future statements are possible
  216. if not (isinstance(prev, astng.From)
  217. and prev.modname == '__future__'):
  218. self.add_message('W0410', node=node)
  219. return
  220. modnode = node.root()
  221. importedmodnode = self.get_imported_module(modnode, node, basename)
  222. if importedmodnode is None:
  223. return
  224. self._check_relative_import(modnode, node, importedmodnode, basename)
  225. self._check_deprecated_module(node, basename)
  226. for name, _ in node.names:
  227. if name == '*':
  228. self.add_message('W0401', args=basename, node=node)
  229. continue
  230. self._add_imported_module(node, '%s.%s' % (importedmodnode.name, name))
  231. self._check_reimport(node, name, basename, node.level)
  232. def get_imported_module(self, modnode, importnode, modname):
  233. try:
  234. return importnode.do_import_module(modname)
  235. except astng.InferenceError, ex:
  236. if str(ex) != modname:
  237. args = '%r (%s)' % (modname, ex)
  238. else:
  239. args = repr(modname)
  240. self.add_message("F0401", args=args, node=importnode)
  241. def _check_relative_import(self, modnode, importnode, importedmodnode,
  242. importedasname):
  243. """check relative import. node is either an Import or From node, modname
  244. the imported module name.
  245. """
  246. if 'W0403' not in self.active_msgs:
  247. return
  248. if importedmodnode.file is None:
  249. return False # built-in module
  250. if modnode is importedmodnode:
  251. return False # module importing itself
  252. if modnode.absolute_import_activated() or getattr(importnode, 'level', None):
  253. return False
  254. if importedmodnode.name != importedasname:
  255. # this must be a relative import...
  256. self.add_message('W0403', args=(importedasname, importedmodnode.name),
  257. node=importnode)
  258. def _add_imported_module(self, node, importedmodname):
  259. """notify an imported module, used to analyze dependencies"""
  260. context_name = node.root().name
  261. if context_name == importedmodname:
  262. # module importing itself !
  263. self.add_message('W0406', node=node)
  264. elif not is_standard_module(importedmodname):
  265. # handle dependencies
  266. importedmodnames = self.stats['dependencies'].setdefault(
  267. importedmodname, set())
  268. if not context_name in importedmodnames:
  269. importedmodnames.add(context_name)
  270. if is_standard_module( importedmodname, (self.package_dir(),) ):
  271. # update import graph
  272. mgraph = self.import_graph.setdefault(context_name, set())
  273. if not importedmodname in mgraph:
  274. mgraph.add(importedmodname)
  275. def _check_deprecated_module(self, node, mod_path):
  276. """check if the module is deprecated"""
  277. for mod_name in self.config.deprecated_modules:
  278. if mod_path == mod_name or mod_path.startswith(mod_name + '.'):
  279. self.add_message('W0402', node=node, args=mod_path)
  280. def _check_reimport(self, node, name, basename=None, level=0):
  281. """check if the import is necessary (i.e. not already done)"""
  282. if 'W0404' not in self.active_msgs:
  283. return
  284. frame = node.frame()
  285. root = node.root()
  286. contexts = [(frame, level)]
  287. if root is not frame:
  288. contexts.append((root, 0))
  289. for context, level in contexts:
  290. first = get_first_import(node, context, name, basename, level)
  291. if first is not None:
  292. self.add_message('W0404', node=node,
  293. args=(name, first.fromlineno))
  294. def report_external_dependencies(self, sect, _, dummy):
  295. """return a verbatim layout for displaying dependencies"""
  296. dep_info = make_tree_defs(self._external_dependencies_info().items())
  297. if not dep_info:
  298. raise EmptyReport()
  299. tree_str = repr_tree_defs(dep_info)
  300. sect.append(VerbatimText(tree_str))
  301. def report_dependencies_graph(self, sect, _, dummy):
  302. """write dependencies as a dot (graphviz) file"""
  303. dep_info = self.stats['dependencies']
  304. if not dep_info or not (self.config.import_graph
  305. or self.config.ext_import_graph
  306. or self.config.int_import_graph):
  307. raise EmptyReport()
  308. filename = self.config.import_graph
  309. if filename:
  310. make_graph(filename, dep_info, sect, '')
  311. filename = self.config.ext_import_graph
  312. if filename:
  313. make_graph(filename, self._external_dependencies_info(),
  314. sect, 'external ')
  315. filename = self.config.int_import_graph
  316. if filename:
  317. make_graph(filename, self._internal_dependencies_info(),
  318. sect, 'internal ')
  319. def _external_dependencies_info(self):
  320. """return cached external dependencies information or build and
  321. cache them
  322. """
  323. if self.__ext_dep_info is None:
  324. self.__ext_dep_info = filter_dependencies_info(
  325. self.stats['dependencies'], self.package_dir(), 'external')
  326. return self.__ext_dep_info
  327. def _internal_dependencies_info(self):
  328. """return cached internal dependencies information or build and
  329. cache them
  330. """
  331. if self.__int_dep_info is None:
  332. self.__int_dep_info = filter_dependencies_info(
  333. self.stats['dependencies'], self.package_dir(), 'internal')
  334. return self.__int_dep_info
  335. def register(linter):
  336. """required method to auto register this checker """
  337. linter.register_checker(ImportsChecker(linter))