summaryrefslogtreecommitdiff
path: root/checkers/imports.py
diff options
context:
space:
mode:
Diffstat (limited to 'checkers/imports.py')
-rw-r--r--checkers/imports.py379
1 files changed, 379 insertions, 0 deletions
diff --git a/checkers/imports.py b/checkers/imports.py
new file mode 100644
index 000000000..6ca248ac8
--- /dev/null
+++ b/checkers/imports.py
@@ -0,0 +1,379 @@
+# Copyright (c) 2003-2005 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
+"""
+
+__revision__ = "$Id: imports.py,v 1.46 2005-11-10 17:26:27 syt Exp $"
+
+from logilab.common import get_cycles
+from logilab.common.modutils import is_standard_module, is_relative, \
+ get_module_part
+from logilab.common.ureports import VerbatimText, Paragraph
+
+from logilab import astng
+
+from pylint.interfaces import IASTNGChecker
+from pylint.checkers import BaseChecker, EmptyReport
+from pylint.checkers.utils import are_exclusive
+
+def get_first_import(context, name, base):
+ """return the node where [base.]<name> is imported or None if not found
+ """
+ for node in context.values():
+ if isinstance(node, astng.Import):
+ if name in [iname[0] for iname in node.names]:
+ return node
+ if isinstance(node, astng.From):
+ if base == node.modname and \
+ name in [iname[0] for iname in node.names]:
+ return node
+
+
+# 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 dictionnary 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 in range(len(nodes)):
+ mod, (sub, files) = nodes[i]
+ 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 dot_node(modname):
+ """return the string representation for a dot node"""
+ return '"%s" [ label="%s" ];' % (modname, modname)
+
+def dot_edge(from_, to_):
+ """return the string representation for a dot edge between two nodes"""
+ return'"%s" -> "%s" [ ] ;' % (from_, to_)
+
+DOT_HEADERS = '''rankdir="LR" URL="." concentrate=false
+edge[fontsize="10" ]
+node[width="0" height="0" fontsize="12" fontcolor="black"]'''
+
+def dependencies_graph(filename, dep_info):
+ """write dependencies as defined in the dep_info dictionary as a dot
+ (graphviz) file
+ """
+ done = {}
+ stream = open(filename, 'w')
+ print >> stream, "digraph g {"
+ print >> stream, DOT_HEADERS
+ for modname, dependencies in dep_info.items():
+ done[modname] = 1
+ print >> stream, dot_node(modname)
+ for modname in dependencies:
+ if not done.has_key(modname):
+ done[modname] = 1
+ print >> stream, dot_node(modname)
+ for depmodname, dependencies in dep_info.items():
+ for modname in dependencies:
+ print >> stream, dot_edge(modname, depmodname)
+ print >> stream,'}'
+ stream.close()
+
+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 %r (%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',
+ '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 R0402 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 R0402 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 R0402 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 = (('R0401', 'External dependencies',
+ self.report_external_dependencies),
+ ('R0402', '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)"""
+ 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"""
+ for name, _ in node.names:
+ self._check_deprecated(node, name)
+ relative = self._check_relative(node, name)
+ self._imported_module(node, name, relative)
+ # handle reimport
+ self._check_reimport(node, name)
+
+
+ def visit_from(self, node):
+ """triggered when an import statement is seen"""
+ basename = node.modname
+ if basename == '__future__':
+ # check this is the first non docstring statement in the module
+ if node.previous_sibling():
+ self.add_message('W0410', node=node)
+ self._check_deprecated(node, basename)
+ relative = self._check_relative(node, basename)
+ for name, _ in node.names:
+ if name == '*':
+ self.add_message('W0401', args=basename, node=node)
+ continue
+ # handle reimport
+ self._check_reimport(node, name, basename)
+ # analyze dependencies
+ fullname = '%s.%s' % (basename, name)
+ if fullname.find('.') > -1:
+ try:
+ # XXXFIXME: don't use get_module_part which doesn't take
+ # care of package precedence
+ fullname = get_module_part(fullname,
+ context_file=node.root().file)
+ except ImportError, ex:
+ self.add_message('F0401', args=(fullname, ex), node=node)
+ continue
+ self._imported_module(node, fullname, relative)
+
+ def _imported_module(self, node, mod_path, relative):
+ """notify an imported module, used to analyze dependencies
+ """
+ context_name = node.root().name
+ if relative:
+ mod_path = '%s.%s' % ('.'.join(context_name.split('.')[:-1]),
+ mod_path)
+ if context_name == mod_path:
+ # module importing itself !
+ self.add_message('W0406', node=node)
+ elif not is_standard_module(mod_path):
+ # handle dependencies
+ mod_paths = self.stats['dependencies'].setdefault(mod_path, [])
+ if not context_name in mod_paths:
+ mod_paths.append(context_name)
+ if is_standard_module( mod_path, (self.package_dir(),) ):
+ # update import graph
+ mgraph = self.import_graph.setdefault(context_name, [])
+ if not mod_path in mgraph:
+ mgraph.append(mod_path)
+
+ def _check_relative(self, node, mod_path):
+ """check relative import module"""
+ # check for relative import
+ context_file = node.root().file
+ relative = is_relative(mod_path, context_file)
+ if relative:
+ self.add_message('W0403', args=mod_path, node=node)
+ return relative
+
+ def _check_deprecated(self, node, mod_path):
+ """check if the module is deprecated"""
+ for mod_name in self.config.deprecated_modules:
+ if mod_path.startswith(mod_name) and \
+ (len(mod_path) == len(mod_name)
+ or mod_path[len(mod_name)] == '.'):
+ self.add_message('W0402', node=node, args=mod_path)
+
+ def _check_reimport(self, node, name, basename=None):
+ """check if the import is necessary (i.e. not already done)
+ """
+ frame = node.frame()
+ first = get_first_import(frame, name, basename)
+ if isinstance(first, (astng.Import, astng.From)) and first is not node \
+ and not are_exclusive(first, node):
+ self.add_message('W0404', node=node, args=(name, first.lineno))
+ else:
+ root = node.root()
+ if root is frame:
+ return
+ first = get_first_import(root, name, basename)
+ if not isinstance(first, (astng.Import, astng.From)):
+ return
+ if first is not node and not are_exclusive(first, node):
+ self.add_message('W0404', node=node,
+ args=(name, first.lineno))
+
+
+ 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))