diff options
-rw-r--r-- | ChangeLog | 3 | ||||
-rw-r--r-- | checkers/imports.py | 184 | ||||
-rw-r--r-- | debian/control | 8 | ||||
-rw-r--r-- | test/messages/func_f0001.txt | 2 | ||||
-rw-r--r-- | test/messages/func_f0401.txt | 2 | ||||
-rw-r--r-- | test/messages/func_names_imported_from_module.txt | 4 | ||||
-rw-r--r-- | test/messages/func_w0233.txt | 2 | ||||
-rw-r--r-- | test/messages/func_w0403.txt | 2 | ||||
-rw-r--r-- | test/messages/func_w0404.txt | 2 | ||||
-rw-r--r-- | test/messages/func_w0406.txt | 1 |
10 files changed, 99 insertions, 111 deletions
@@ -3,9 +3,10 @@ ChangeLog for PyLint -- * improved flymake code and doc provided by Derek Harland + * refactor & fix the imports checker * fix #8764: More than one statement on a single line false positive with try/except/finally - * Nathaniel's fix for w0108 false positive + * Nathaniel's fix for w0108 false positive 2009-03-25 -- 0.18.0 * tests ok with python 2.4, 2.5, 2.6. 2.3 not tested diff --git a/checkers/imports.py b/checkers/imports.py index feff6c1..1eef1f3 100644 --- a/checkers/imports.py +++ b/checkers/imports.py @@ -13,19 +13,16 @@ # 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 -""" +"""imports checkers for Python code""" from logilab.common.graph import get_cycles, DotBackend -from logilab.common.modutils import is_standard_module, is_relative, \ - get_module_part +from logilab.common.modutils import is_standard_module from logilab.common.ureports import VerbatimText, Paragraph from logilab.common.compat import sorted, enumerate from logilab import astng from logilab.astng.infutils import are_exclusive -from pylint.utils import expand_modules from pylint.interfaces import IASTNGChecker from pylint.checkers import BaseChecker, EmptyReport @@ -41,8 +38,8 @@ def get_first_import(context, name, base, level=0): if base == node.modname and level == node.level 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'): @@ -96,7 +93,7 @@ def repr_tree_defs(data, indent_str=None): def dependencies_graph(filename, dep_info): - """write dependencies as a dot (graphviz) file + """write dependencies as a dot (graphviz) file """ done = {} printer = DotBackend(filename[:-4], rankdir = "LR") @@ -126,43 +123,43 @@ def make_graph(filename, dep_info, sect, gtype): # the import checker itself ################################################### MSGS = { - 'F0401': ('Unable to import %r (%s)' , + 'F0401': ('Unable to import %r' , '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', + '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 + """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'), @@ -192,7 +189,7 @@ given file (report R0402 must not be disabled)'} 'help' : 'Create a graph of internal dependencies in the \ given file (report R0402 must not be disabled)'} ), - + ) def __init__(self, linter=None): @@ -205,38 +202,37 @@ given file (report R0402 must not be disabled)'} ('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)""" # 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: - if self._module_not_exists(node, name): + importedmodnode = self.get_imported_module(modnode, node, name) + if importedmodnode is None: continue - self._check_deprecated(node, name) - relative = self._check_relative(node, name) - self._imported_module(node, name, relative) - # handle reimport + 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 self._module_not_exists(node, basename): - return if basename == '__future__': # check if this is the first non-docstring statement in the module prev = node.previous_sibling() @@ -245,83 +241,76 @@ given file (report R0402 must not be disabled)'} if not (isinstance(prev, astng.From) and prev.modname == '__future__'): self.add_message('W0410', node=node) - self._check_deprecated(node, basename) - level = node.level - if level > 0: # explicit relative import (leading dots) - relative = True - else: - relative = self._check_relative(node, basename) + 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 - # handle reimport - self._check_reimport(node, name, basename, level) - # analyze dependencies - fullname = '.' * level + '%s.%s' % (basename, name) - fullname = get_module_part(fullname,context_file=node.root().file) - self._imported_module(node, fullname, relative) - - def _module_not_exists(self, node, modname): - """check if module exists and possibly add message""" - errors = expand_modules([modname], [])[1] - if not errors or is_relative(modname, node.root().file): - return False - error = errors[0] - if error["key"] == "F0001": - args = (error["mod"], error["ex"]) - self.add_message("F0401", args=args, node=node) - return True - assert error["key"] == "F0003" - return False - - def _imported_module(self, node, mod_path, relative): - """notify an imported module, used to analyze dependencies + 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).startswith('module importing itself'): # XXX + return modnode + else: + self.add_message("F0401", args=modname, node=importnode) + return + + 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 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 relative: - context_parts = context_name.split('.') - if mod_path.startswith('.'): - while mod_path.startswith('.'): - mod_path = mod_path[1:] - del context_parts[-1] # one level upwards - context_parts.append(mod_path) - else: - context_parts[-1] = mod_path - mod_path = '.'.join(context_parts) - if context_name == mod_path: + if context_name == importedmodname: # module importing itself ! self.add_message('W0406', node=node) - elif not is_standard_module(mod_path): + elif not is_standard_module(importedmodname): # 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(),) ): + 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, []) - if not mod_path in mgraph: - mgraph.append(mod_path) - - def _check_relative(self, node, mod_path): - """check relative import module""" - 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): + 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""" + # XXX rewrite 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, level=0): - """check if the import is necessary (i.e. not already done) - """ + """check if the import is necessary (i.e. not already done)""" + # XXX rewrite frame = node.frame() first = get_first_import(frame, name, basename, level) if isinstance(first, (astng.Import, astng.From)) and first is not node \ @@ -338,10 +327,9 @@ given file (report R0402 must not be disabled)'} self.add_message('W0404', node=node, args=(name, first.fromlineno)) - + def report_external_dependencies(self, sect, _, dummy): - """return a verbatim layout for displaying dependencies - """ + """return a verbatim layout for displaying dependencies""" dep_info = make_tree_defs(self._external_dependencies_info().items()) if not dep_info: raise EmptyReport() @@ -350,7 +338,7 @@ given file (report R0402 must not be disabled)'} def report_dependencies_graph(self, sect, _, dummy): """write dependencies as a dot (graphviz) file""" - dep_info = self.stats['dependencies'] + 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): @@ -366,7 +354,7 @@ given file (report R0402 must not be disabled)'} 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 @@ -375,7 +363,7 @@ given file (report R0402 must not be disabled)'} 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 @@ -384,8 +372,8 @@ given file (report R0402 must not be disabled)'} 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)) diff --git a/debian/control b/debian/control index a8c351e..ca92747 100644 --- a/debian/control +++ b/debian/control @@ -13,7 +13,7 @@ Vcs-Browser: http://svn.debian.org/viewsvn/python-apps/packages/pylint/trunk/ Package: pylint Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, python-logilab-common (>= 0.39.0), python-logilab-astng (>= 0.19.0) +Depends: ${python:Depends}, ${misc:Depends}, python-logilab-common (>= 0.39.0), python-logilab-astng (>= 0.19.1) Recommends: python-tk XB-Python-Version: ${python:Versions} Conflicts: python2.2-pylint, python2.3-pylint, python2.4-pylint, pylint-common, pylint-test @@ -31,9 +31,9 @@ Description: python code static checker and UML diagram generator and much more. . Additionally, it is possible to write plugins to add your own checks. - . - The included command pyreverse generates UML class and package + . + The included command pyreverse generates UML class and package diagrams. - . + . The recommended python-tk package is only for using the pylint-gui script. diff --git a/test/messages/func_f0001.txt b/test/messages/func_f0001.txt index 2c97661..911d805 100644 --- a/test/messages/func_f0001.txt +++ b/test/messages/func_f0001.txt @@ -1,2 +1,2 @@ -F: 3: Unable to import 'whatever' (No module named whatever) +F: 3: Unable to import 'whatever' W: 3: Unused import whatever diff --git a/test/messages/func_f0401.txt b/test/messages/func_f0401.txt index e10c2d8..27dc386 100644 --- a/test/messages/func_f0401.txt +++ b/test/messages/func_f0401.txt @@ -1,2 +1,2 @@ -F: 8:function: Unable to import 'tutu' (No module named tutu) +F: 8:function: Unable to import 'tutu' diff --git a/test/messages/func_names_imported_from_module.txt b/test/messages/func_names_imported_from_module.txt index 5ed3d7d..af4e26e 100644 --- a/test/messages/func_names_imported_from_module.txt +++ b/test/messages/func_names_imported_from_module.txt @@ -6,5 +6,5 @@ E: 13: Module 'logilab.common.modutils' has no 'yo' member E: 17: Module 'sys' has no 'stdoout' member E: 24: No name 'compiile' in module 're' E: 24: No name 'findiiter' in module 're' -F: 6: Unable to import 'logilab.common.tutu' (No module named tutu) -F: 23: Unable to import 'rie' (No module named rie) +F: 6: Unable to import 'logilab.common.tutu' +F: 23: Unable to import 'rie' diff --git a/test/messages/func_w0233.txt b/test/messages/func_w0233.txt index 4163dce..9e2845f 100644 --- a/test/messages/func_w0233.txt +++ b/test/messages/func_w0233.txt @@ -1,2 +1,2 @@ -F: 20: Unable to import 'nonexistant' (No module named nonexistant) +F: 20: Unable to import 'nonexistant' W: 12:AAAA.__init__: __init__ method from a non direct base class 'BBBBMixin' is called diff --git a/test/messages/func_w0403.txt b/test/messages/func_w0403.txt index 23f4667..70679ec 100644 --- a/test/messages/func_w0403.txt +++ b/test/messages/func_w0403.txt @@ -1,2 +1,2 @@ -F: 11: Unable to import 'stringfile' (No module named stringfile) +F: 11: Unable to import 'stringfile' W: 8: Uses of a deprecated module 'Bastion' diff --git a/test/messages/func_w0404.txt b/test/messages/func_w0404.txt index 311056d..f2f29b3 100644 --- a/test/messages/func_w0404.txt +++ b/test/messages/func_w0404.txt @@ -1 +1 @@ -W: 6: Relative import 'func_w0302' +W: 6: Relative import 'func_w0302', should be 'input.func_w0302' diff --git a/test/messages/func_w0406.txt b/test/messages/func_w0406.txt index a1440b9..6145515 100644 --- a/test/messages/func_w0406.txt +++ b/test/messages/func_w0406.txt @@ -1,2 +1 @@ W: 5: Module import itself -W: 5: Relative import 'func_w0406' |