Index: third_party/pylint/checkers/imports.py |
diff --git a/third_party/pylint/checkers/imports.py b/third_party/pylint/checkers/imports.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..7e6a4f880c9cc9b4bcb4ae5135df31315b426775 |
--- /dev/null |
+++ b/third_party/pylint/checkers/imports.py |
@@ -0,0 +1,378 @@ |
+# Copyright (c) 2003-2010 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. |
+"""imports checkers for Python code""" |
+ |
+from logilab.common.graph import get_cycles, DotBackend |
+from logilab.common.modutils import is_standard_module |
+from logilab.common.ureports import VerbatimText, Paragraph |
+ |
+from logilab import astng |
+from logilab.astng import are_exclusive |
+ |
+from pylint.interfaces import IASTNGChecker |
+from pylint.checkers import BaseChecker, EmptyReport |
+ |
+ |
+def get_first_import(node, context, name, base, level): |
+ """return the node where [base.]<name> is imported or None if not found |
+ """ |
+ first = None |
+ found = False |
+ for first in context.values(): |
+ if isinstance(first, astng.Import): |
+ if name in [iname[0] for iname in first.names]: |
+ found = True |
+ break |
+ elif isinstance(first, astng.From): |
+ if base == first.modname and level == first.level and \ |
+ name in [iname[0] for iname in first.names]: |
+ found = True |
+ break |
+ if found and first is not node and not are_exclusive(first, node): |
+ return first |
+ |
+# utilities to represents import dependencies as tree and dot graph ########### |
+ |
+def filter_dependencies_info(dep_info, package_dir, mode='external'): |
+ """filter external or internal dependencies from dep_info (return a |
+ new dictionary containing the filtered modules only) |
+ """ |
+ if mode == 'external': |
+ filter_func = lambda x: not is_standard_module(x, (package_dir,)) |
+ else: |
+ assert mode == 'internal' |
+ filter_func = lambda x: is_standard_module(x, (package_dir,)) |
+ result = {} |
+ for importee, importers in dep_info.items(): |
+ if filter_func(importee): |
+ result[importee] = importers |
+ return result |
+ |
+def make_tree_defs(mod_files_list): |
+ """get a list of 2-uple (module, list_of_files_which_import_this_module), |
+ it will return a dictionary to represent this as a tree |
+ """ |
+ tree_defs = {} |
+ for mod, files in mod_files_list: |
+ node = (tree_defs, ()) |
+ for prefix in mod.split('.'): |
+ node = node[0].setdefault(prefix, [{}, []]) |
+ node[1] += files |
+ return tree_defs |
+ |
+def repr_tree_defs(data, indent_str=None): |
+ """return a string which represents imports as a tree""" |
+ lines = [] |
+ nodes = data.items() |
+ for i, (mod, (sub, files)) in enumerate(sorted(nodes, key=lambda x: x[0])): |
+ if not files: |
+ files = '' |
+ else: |
+ files = '(%s)' % ','.join(files) |
+ if indent_str is None: |
+ lines.append('%s %s' % (mod, files)) |
+ sub_indent_str = ' ' |
+ else: |
+ lines.append('%s\-%s %s' % (indent_str, mod, files)) |
+ if i == len(nodes)-1: |
+ sub_indent_str = '%s ' % indent_str |
+ else: |
+ sub_indent_str = '%s| ' % indent_str |
+ if sub: |
+ lines.append(repr_tree_defs(sub, sub_indent_str)) |
+ return '\n'.join(lines) |
+ |
+ |
+def dependencies_graph(filename, dep_info): |
+ """write dependencies as a dot (graphviz) file |
+ """ |
+ done = {} |
+ printer = DotBackend(filename[:-4], rankdir = "LR") |
+ printer.emit('URL="." node[shape="box"]') |
+ for modname, dependencies in dep_info.items(): |
+ done[modname] = 1 |
+ printer.emit_node(modname) |
+ for modname in dependencies: |
+ if modname not in done: |
+ done[modname] = 1 |
+ printer.emit_node(modname) |
+ for depmodname, dependencies in dep_info.items(): |
+ for modname in dependencies: |
+ printer.emit_edge(modname, depmodname) |
+ printer.generate(filename) |
+ |
+ |
+def make_graph(filename, dep_info, sect, gtype): |
+ """generate a dependencies graph and add some information about it in the |
+ report's section |
+ """ |
+ dependencies_graph(filename, dep_info) |
+ sect.append(Paragraph('%simports graph has been written to %s' |
+ % (gtype, filename))) |
+ |
+ |
+# the import checker itself ################################################### |
+ |
+MSGS = { |
+ 'F0401': ('Unable to import %s', |
+ 'Used when pylint has been unable to import a module.'), |
+ 'R0401': ('Cyclic import (%s)', |
+ 'Used when a cyclic import between two or more modules is \ |
+ detected.'), |
+ |
+ 'W0401': ('Wildcard import %s', |
+ 'Used when `from module import *` is detected.'), |
+ 'W0402': ('Uses of a deprecated module %r', |
+ 'Used a module marked as deprecated is imported.'), |
+ 'W0403': ('Relative import %r, should be %r', |
+ 'Used when an import relative to the package directory is \ |
+ detected.'), |
+ 'W0404': ('Reimport %r (imported line %s)', |
+ 'Used when a module is reimported multiple times.'), |
+ 'W0406': ('Module import itself', |
+ 'Used when a module is importing itself.'), |
+ |
+ 'W0410': ('__future__ import is not the first non docstring statement', |
+ 'Python 2.5 and greater require __future__ import to be the \ |
+ first non docstring statement in the module.'), |
+ } |
+ |
+class ImportsChecker(BaseChecker): |
+ """checks for |
+ * external modules dependencies |
+ * relative / wildcard imports |
+ * cyclic imports |
+ * uses of deprecated modules |
+ """ |
+ |
+ __implements__ = IASTNGChecker |
+ |
+ name = 'imports' |
+ msgs = MSGS |
+ priority = -2 |
+ |
+ options = (('deprecated-modules', |
+ {'default' : ('regsub', 'string', 'TERMIOS', |
+ 'Bastion', 'rexec'), |
+ 'type' : 'csv', |
+ 'metavar' : '<modules>', |
+ 'help' : 'Deprecated modules which should not be used, \ |
+separated by a comma'} |
+ ), |
+ ('import-graph', |
+ {'default' : '', |
+ 'type' : 'string', |
+ 'metavar' : '<file.dot>', |
+ 'help' : 'Create a graph of every (i.e. internal and \ |
+external) dependencies in the given file (report RP0402 must not be disabled)'} |
+ ), |
+ ('ext-import-graph', |
+ {'default' : '', |
+ 'type' : 'string', |
+ 'metavar' : '<file.dot>', |
+ 'help' : 'Create a graph of external dependencies in the \ |
+given file (report RP0402 must not be disabled)'} |
+ ), |
+ ('int-import-graph', |
+ {'default' : '', |
+ 'type' : 'string', |
+ 'metavar' : '<file.dot>', |
+ 'help' : 'Create a graph of internal dependencies in the \ |
+given file (report RP0402 must not be disabled)'} |
+ ), |
+ |
+ ) |
+ |
+ def __init__(self, linter=None): |
+ BaseChecker.__init__(self, linter) |
+ self.stats = None |
+ self.import_graph = None |
+ self.__int_dep_info = self.__ext_dep_info = None |
+ self.reports = (('RP0401', 'External dependencies', |
+ self.report_external_dependencies), |
+ ('RP0402', 'Modules dependencies graph', |
+ self.report_dependencies_graph), |
+ ) |
+ |
+ def open(self): |
+ """called before visiting project (i.e set of modules)""" |
+ self.linter.add_stats(dependencies={}) |
+ self.linter.add_stats(cycles=[]) |
+ self.stats = self.linter.stats |
+ self.import_graph = {} |
+ |
+ def close(self): |
+ """called before visiting project (i.e set of modules)""" |
+ # don't try to compute cycles if the associated message is disabled |
+ if self.linter.is_message_enabled('R0401'): |
+ for cycle in get_cycles(self.import_graph): |
+ self.add_message('R0401', args=' -> '.join(cycle)) |
+ |
+ def visit_import(self, node): |
+ """triggered when an import statement is seen""" |
+ modnode = node.root() |
+ for name, _ in node.names: |
+ importedmodnode = self.get_imported_module(modnode, node, name) |
+ if importedmodnode is None: |
+ continue |
+ self._check_relative_import(modnode, node, importedmodnode, name) |
+ self._add_imported_module(node, importedmodnode.name) |
+ self._check_deprecated_module(node, name) |
+ self._check_reimport(node, name) |
+ |
+ |
+ def visit_from(self, node): |
+ """triggered when a from statement is seen""" |
+ basename = node.modname |
+ if basename == '__future__': |
+ # check if this is the first non-docstring statement in the module |
+ prev = node.previous_sibling() |
+ if prev: |
+ # consecutive future statements are possible |
+ if not (isinstance(prev, astng.From) |
+ and prev.modname == '__future__'): |
+ self.add_message('W0410', node=node) |
+ return |
+ modnode = node.root() |
+ importedmodnode = self.get_imported_module(modnode, node, basename) |
+ if importedmodnode is None: |
+ return |
+ self._check_relative_import(modnode, node, importedmodnode, basename) |
+ self._check_deprecated_module(node, basename) |
+ for name, _ in node.names: |
+ if name == '*': |
+ self.add_message('W0401', args=basename, node=node) |
+ continue |
+ self._add_imported_module(node, '%s.%s' % (importedmodnode.name, name)) |
+ self._check_reimport(node, name, basename, node.level) |
+ |
+ def get_imported_module(self, modnode, importnode, modname): |
+ try: |
+ return importnode.do_import_module(modname) |
+ except astng.InferenceError, ex: |
+ if str(ex) != modname: |
+ args = '%r (%s)' % (modname, ex) |
+ else: |
+ args = repr(modname) |
+ self.add_message("F0401", args=args, node=importnode) |
+ |
+ def _check_relative_import(self, modnode, importnode, importedmodnode, |
+ importedasname): |
+ """check relative import. node is either an Import or From node, modname |
+ the imported module name. |
+ """ |
+ if 'W0403' not in self.active_msgs: |
+ return |
+ if importedmodnode.file is None: |
+ return False # built-in module |
+ if modnode is importedmodnode: |
+ return False # module importing itself |
+ if modnode.absolute_import_activated() or getattr(importnode, 'level', None): |
+ return False |
+ if importedmodnode.name != importedasname: |
+ # this must be a relative import... |
+ self.add_message('W0403', args=(importedasname, importedmodnode.name), |
+ node=importnode) |
+ |
+ def _add_imported_module(self, node, importedmodname): |
+ """notify an imported module, used to analyze dependencies""" |
+ context_name = node.root().name |
+ if context_name == importedmodname: |
+ # module importing itself ! |
+ self.add_message('W0406', node=node) |
+ elif not is_standard_module(importedmodname): |
+ # handle dependencies |
+ importedmodnames = self.stats['dependencies'].setdefault( |
+ importedmodname, set()) |
+ if not context_name in importedmodnames: |
+ importedmodnames.add(context_name) |
+ if is_standard_module( importedmodname, (self.package_dir(),) ): |
+ # update import graph |
+ mgraph = self.import_graph.setdefault(context_name, set()) |
+ if not importedmodname in mgraph: |
+ mgraph.add(importedmodname) |
+ |
+ def _check_deprecated_module(self, node, mod_path): |
+ """check if the module is deprecated""" |
+ for mod_name in self.config.deprecated_modules: |
+ if mod_path == mod_name or mod_path.startswith(mod_name + '.'): |
+ self.add_message('W0402', node=node, args=mod_path) |
+ |
+ def _check_reimport(self, node, name, basename=None, level=0): |
+ """check if the import is necessary (i.e. not already done)""" |
+ if 'W0404' not in self.active_msgs: |
+ return |
+ frame = node.frame() |
+ root = node.root() |
+ contexts = [(frame, level)] |
+ if root is not frame: |
+ contexts.append((root, 0)) |
+ for context, level in contexts: |
+ first = get_first_import(node, context, name, basename, level) |
+ if first is not None: |
+ self.add_message('W0404', node=node, |
+ args=(name, first.fromlineno)) |
+ |
+ |
+ def report_external_dependencies(self, sect, _, dummy): |
+ """return a verbatim layout for displaying dependencies""" |
+ dep_info = make_tree_defs(self._external_dependencies_info().items()) |
+ if not dep_info: |
+ raise EmptyReport() |
+ tree_str = repr_tree_defs(dep_info) |
+ sect.append(VerbatimText(tree_str)) |
+ |
+ def report_dependencies_graph(self, sect, _, dummy): |
+ """write dependencies as a dot (graphviz) file""" |
+ dep_info = self.stats['dependencies'] |
+ if not dep_info or not (self.config.import_graph |
+ or self.config.ext_import_graph |
+ or self.config.int_import_graph): |
+ raise EmptyReport() |
+ filename = self.config.import_graph |
+ if filename: |
+ make_graph(filename, dep_info, sect, '') |
+ filename = self.config.ext_import_graph |
+ if filename: |
+ make_graph(filename, self._external_dependencies_info(), |
+ sect, 'external ') |
+ filename = self.config.int_import_graph |
+ if filename: |
+ make_graph(filename, self._internal_dependencies_info(), |
+ sect, 'internal ') |
+ |
+ def _external_dependencies_info(self): |
+ """return cached external dependencies information or build and |
+ cache them |
+ """ |
+ if self.__ext_dep_info is None: |
+ self.__ext_dep_info = filter_dependencies_info( |
+ self.stats['dependencies'], self.package_dir(), 'external') |
+ return self.__ext_dep_info |
+ |
+ def _internal_dependencies_info(self): |
+ """return cached internal dependencies information or build and |
+ cache them |
+ """ |
+ if self.__int_dep_info is None: |
+ self.__int_dep_info = filter_dependencies_info( |
+ self.stats['dependencies'], self.package_dir(), 'internal') |
+ return self.__int_dep_info |
+ |
+ |
+def register(linter): |
+ """required method to auto register this checker """ |
+ linter.register_checker(ImportsChecker(linter)) |